diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6bb853b994..e8fc6cbafc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,24 +1,21 @@ name: Report a bug -description: Let us know if something's not working the way you expected. +description: For regressions only (things that were working earlier) labels: [] body: - type: markdown attributes: value: | - Before opening a new bug report, please ensure - 1. you are on the latest version (it might've already been fixed), - 2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate). - - If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead. + Before opening a new issue, **please** ensure + 1. You are on the latest version, + 2. You've searched for existing issues, + 3. It was working earlier (otherwise use [this](https://github.com/ente-io/ente/discussions/categories/enhancements)) + 4. It is not about self hosting (otherwise use [this](https://github.com/ente-io/ente/discussions/categories/q-a)) - type: textarea attributes: label: Description description: > - Please describe the bug. If possible, also include the steps to - reproduce the behaviour, and the expected behaviour (sometimes - bugs are just expectation mismatches, in which case this would be - a good fit for [feature - requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)). + Describe the bug and steps to reproduce the behaviour, and how it + differs from the previously working behaviour. validations: required: true - type: input @@ -30,15 +27,12 @@ body: attributes: label: Last working version description: > - The version where the feature was last known to be working. It is - fine if you don't remember the exact version (mention roughly - then), but if there just isn't a last known working version, then - it is likely that what is being reported is not an issue but a - feature request. The difference between the two categories is not - just semantic - feature requests use GitHub discussions and so can - be [upvoted by the - community](https://github.com/ente-io/ente/discussions/categories/feature-requests) - (issues can't be). + The version where things were last known to be working. It is fine + if you don't remember the exact version (mention roughly then), + but **if there just isn't a last working version, then please file + it as an + [enhancement](https://github.com/ente-io/ente/discussions/categories/enhancements))** + (where the community upvotes can be used to help prioritize). placeholder: e.g. v1.2.3 - type: dropdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4fae579410..cc9ab42e35 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - - name: Feature requests and questions + - name: Enhacements, feature requests, feedback and questions url: https://github.com/ente-io/ente/discussions about: Please use Discussions for everything apart from the above. diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index 697c08efd2..97d079a681 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -36,7 +36,7 @@ permissions: jobs: build-linux-latest: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 defaults: run: @@ -83,7 +83,7 @@ jobs: # disable this step if release tag contains nightly or beta if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta') run: | - flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore + flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore --dart-define=cronetHttpNoPlay=true env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} @@ -93,7 +93,7 @@ jobs: - name: Install dependencies for desktop build run: | sudo apt-get update -y - sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff6 xz-utils libarchive-tools libcurl4-openssl-dev + sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools libcurl4-openssl-dev sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' - name: Install appimagetool @@ -117,7 +117,9 @@ jobs: mv dist/**/*-*-linux.deb artifacts/ente-${{ github.ref_name }}-x86_64.deb - name: Generate checksums - run: sha256sum artifacts/ente-* >> artifacts/sha256sum-rpm-appimage + run: | + sha256sum artifacts/ente-auth-*.apk >> artifacts/sha256sum-apk + sha256sum artifacts/ente-auth-*.deb artifacts/ente-auth-*.rpm artifacts/ente-auth-*.AppImage >> artifacts/sha256sum-linux - name: Create a draft GitHub release uses: ncipollo/release-action@v1 @@ -139,6 +141,7 @@ jobs: build-windows: runs-on: windows-latest + environment: "auth-win-build" defaults: run: @@ -172,14 +175,22 @@ jobs: - name: Retain Windows EXE and DLLs run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows - - name: Code sign Windows installer and EXE - uses: dlemstra/code-sign-action@v1 + - name: Sign files with Trusted Signing + uses: azure/trusted-signing-action@v0 with: - certificate: "${{ secrets.WINDOWS_CERTIFICATE }}" - password: "${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}" - files: | - auth/artifacts/ente-${{ github.ref_name }}-installer.exe - auth/ente-${{ github.ref_name }}-windows/auth.exe + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files: | + ${{ github.workspace }}/auth/artifacts/ente-${{ github.ref_name }}-installer.exe + ${{ github.workspace }}/auth/ente-${{ github.ref_name }}-windows/auth.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: Zip Windows EXE and DLLs run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows diff --git a/.github/workflows/auth-win-sign.yml b/.github/workflows/auth-win-sign.yml new file mode 100644 index 0000000000..860cde2ca2 --- /dev/null +++ b/.github/workflows/auth-win-sign.yml @@ -0,0 +1,70 @@ +name: "Windows build & Sign (auth)" + + +on: + workflow_dispatch: # Allow manually running the action + +env: + FLUTTER_VERSION: "3.24.3" + +permissions: + contents: write + +jobs: + build-windows: + runs-on: windows-latest + environment: "auth-win-build" + + defaults: + run: + working-directory: auth + + steps: + - name: Checkout code and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Flutter ${{ env.FLUTTER_VERSION }} + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Create artifacts directory + run: mkdir artifacts + + - name: Build Windows installer + run: | + flutter config --enable-windows-desktop + # dart pub global activate flutter_distributor + dart pub global activate --source git https://github.com/ente-io/flutter_distributor_fork --git-ref develop --git-path packages/flutter_distributor + make innoinstall + flutter_distributor package --platform=windows --targets=exe --skip-clean + mv dist/**/*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe + + - name: Retain Windows EXE and DLLs + run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows + + - name: Sign files with Trusted Signing + uses: azure/trusted-signing-action@v0 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files: | + ${{ github.workspace }}/auth/artifacts/ente-${{ github.ref_name }}-installer.exe + ${{ github.workspace }}/auth/ente-${{ github.ref_name }}-windows/auth.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Zip Windows EXE and DLLs + run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows + + - name: Generate checksums + run: sha256sum artifacts/ente-* > artifacts/sha256sum-windows diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml new file mode 100644 index 0000000000..f227f2115b --- /dev/null +++ b/.github/workflows/docs-lint.yml @@ -0,0 +1,32 @@ +name: "Lint (docs)" + +on: + # Run on every pull request (open or push to it) that changes docs/ + pull_request: + paths: + - "docs/**" + - ".github/workflows/docs-lint.yml" + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "yarn" + cache-dependency-path: "web/yarn.lock" + + - run: yarn install + + - run: yarn pretty:check diff --git a/.github/workflows/mobile-internal-release-rust.yml b/.github/workflows/mobile-internal-release-rust.yml new file mode 100644 index 0000000000..aa254f2268 --- /dev/null +++ b/.github/workflows/mobile-internal-release-rust.yml @@ -0,0 +1,77 @@ +name: "Internal release (photos)" + +on: + workflow_dispatch: # Allow manually running the action + +env: + FLUTTER_VERSION: "3.24.3" + RUST_VERSION: "1.85.1" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mobile + + steps: + - name: Checkout code and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Install Flutter ${{ env.FLUTTER_VERSION }} + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install Rust ${{ env.RUST_VERSION }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Install Flutter Rust Bridge + run: cargo install flutter_rust_bridge_codegen + + - name: Setup keys + uses: timheuer/base64-to-file@v1 + with: + fileName: "keystore/ente_photos_key.jks" + encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }} + + - name: Build PlayStore AAB + run: | + flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore + env: + SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }} + + - name: Upload AAB to PlayStore + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: io.ente.photos + releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab + track: internal + + - name: Notify Discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} + nodetail: true + title: "🏆 Internal release available for Photos" + description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)" + color: 0x00ff00 diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index b8ef0b2225..f1d5724f97 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -63,6 +63,6 @@ jobs: with: webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} nodetail: true - title: "🏆 Internal release available for Photos" + title: "🏆 Internal release Photos (Branch: ${{ github.ref_name }})" description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)" color: 0x00ff00 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80e6424187..242cc2b65c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Just hang around, enjoy the vibe. Answer someone's query on our [Discord](https://discord.gg/z2YVKkycX3), or pile on in the sporadic #off-topic rants there. Chuckle (or wince!) at our [Twitter](https://twitter.com/enteio) memes. Suggest a new feature in our [Github -Discussions](https://github.com/ente-io/ente/discussions/new?category=feature-requests), +Discussions](https://github.com/ente-io/ente/discussions/new?category=enhancements), or upvote the existing ones that you feel we should focus on first. Provide your opinion on existing threads. @@ -68,8 +68,8 @@ best to start small. Consider some well-scoped changes, say like adding more Each of the individual product/platform specific directories in this repository have instructions on setting up a dev environment. -For anything beyond trivial bug fixes, please use [features requests and -discussions](https://github.com/ente-io/ente/discussions) instead of performing +For anything beyond trivial bug fixes, please use +[discussions](https://github.com/ente-io/ente/discussions) instead of performing code changes directly. > [!TIP] diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index f25bcc24b4..031df9e430 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -39,11 +39,24 @@ "title": "Ankama", "slug": "ankama" }, + { + "title": "Ankara University", + "slug": "ankara_university", + "altNames": [ + "Ankara Üniversitesi" + ] + }, { "title": "Anycoin Direct", "slug": "anycoindirect" }, - { + { + "title": "AR24", + "altNames": [ + "Docaposte AR24" + ] + }, + { "title": "Aruba", "slug": "aruba", "hex": "ef8a33" @@ -132,6 +145,10 @@ "Binance US" ] }, + { + "title": "Bitazza", + "slug": "bitazza" + }, { "title": "Bitkub", "slug": "bitkub" @@ -188,7 +205,11 @@ "slug": "blue_sky" }, { - "title": "bonify" + "title": "bonify", + "slug": "bonify", + "altNames": [ + "bonify.de" + ] }, { "title": "Booking", @@ -292,6 +313,15 @@ { "title": "CSAM" }, + { + "title": "CSSBuy", + "slug": "cssbuy", + "altNames": [ + "CSS Buy", + "CSS-Buy", + "cssbuy.com" + ] + }, { "title": "CSFloat" }, @@ -299,6 +329,10 @@ "title": "CSGORoll", "slug": "csgoroll" }, + { + "title": "Cryptee", + "slug": "cryptee" + }, { "title": "Cwallet", "altNames": [ @@ -431,6 +465,9 @@ "title": "Finanzfluss", "slug": "finanzfluss" }, + { + "title": "Finary" + }, { "title": "Firefox", "slug": "mozilla" @@ -445,6 +482,16 @@ "title": "FreeTaxUSA", "slug": "freetaxusa" }, + { + "title": "FZJ", + "slug": "fzj", + "hex": "023d6b", + "altNames": [ + "Forschungszentrum Jülich", + "FZJ IdP", + "iffLogin" + ] + }, { "title": "G2A" }, @@ -452,6 +499,9 @@ "title": "Gate.io", "slug": "gateio.svg" }, + { + "title": "GERID" + }, { "title": "GitHub" }, @@ -706,8 +756,8 @@ ] }, { - "title": "Mercado Livre", - "slug": "mercado_livre", + "title": "Mercado Libre", + "slug": "mercado_libre", "altNames": [ "Mercado Libre", "MercadoLibre", @@ -733,6 +783,7 @@ { "title": "Mistral", "altNames": [ + "Le Chat", "Mistral AI", "MistralAI" ] @@ -877,11 +928,15 @@ "欧易" ] }, - { + { "title": "OnShape", "slug": "onshape", "hex": "7abb5e" }, + { + "title": "Oracle Cloud", + "slug": "oracle_cloud" + }, { "title": "Parqet", "slug": "parqet" @@ -982,6 +1037,14 @@ "qiniu" ] }, + { + "title": "R10.net", + "slug": "r10", + "altNames": [ + "R10", + "r10.net" + ] + }, { "title": "Raindrop.io", "slug": "raindrop_io", @@ -1001,7 +1064,7 @@ "title": "RealMe", "slug": "realme" }, - { + { "title": "RealVNC", "slug": "realvnc", "hex": "488aec" @@ -1268,6 +1331,14 @@ "title": "US Mobile", "slug": "us_mobile" }, + { + "title": "uollet", + "slug": "uollet", + "altNames": [ + "UOLLET", + "uollet.com.br" + ] + }, { "title": "Vikunja" }, @@ -1353,6 +1424,34 @@ }, { "title": "CoinSpot" + }, + { + "title": "Aternos", + "slug": "aternos" + }, + { + "title": "Toshl Finance", + "slug": "toshl_finance", + "altNames": [ + "Toshl" + ] + }, + { + "title": "Tebex", + "slug": "tebex", + "altNames": [ + "tebex", + "tebex.io", + "buycraft" + ] + }, + { + "title": "xAI", + "slug": "xai" + }, + { + "title": "Cronometer", + "slug": "cronometer" } ] } diff --git a/auth/assets/custom-icons/icons/ankara_university.svg b/auth/assets/custom-icons/icons/ankara_university.svg new file mode 100644 index 0000000000..2108a14a66 --- /dev/null +++ b/auth/assets/custom-icons/icons/ankara_university.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/ar24.svg b/auth/assets/custom-icons/icons/ar24.svg new file mode 100644 index 0000000000..31875c3048 --- /dev/null +++ b/auth/assets/custom-icons/icons/ar24.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/aternos.svg b/auth/assets/custom-icons/icons/aternos.svg new file mode 100644 index 0000000000..8f5c5ddbc0 --- /dev/null +++ b/auth/assets/custom-icons/icons/aternos.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/bitazza.svg b/auth/assets/custom-icons/icons/bitazza.svg new file mode 100644 index 0000000000..4433d6d261 --- /dev/null +++ b/auth/assets/custom-icons/icons/bitazza.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/bonify.svg b/auth/assets/custom-icons/icons/bonify.svg index 0116be5044..2c12177f3a 100644 --- a/auth/assets/custom-icons/icons/bonify.svg +++ b/auth/assets/custom-icons/icons/bonify.svg @@ -1,29 +1,14 @@ - - - - - - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/cronometer.svg b/auth/assets/custom-icons/icons/cronometer.svg new file mode 100644 index 0000000000..9037cc8e50 --- /dev/null +++ b/auth/assets/custom-icons/icons/cronometer.svg @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/auth/assets/custom-icons/icons/cryptee.svg b/auth/assets/custom-icons/icons/cryptee.svg new file mode 100644 index 0000000000..1f2a5567ab --- /dev/null +++ b/auth/assets/custom-icons/icons/cryptee.svg @@ -0,0 +1 @@ + diff --git a/auth/assets/custom-icons/icons/cssbuy.svg b/auth/assets/custom-icons/icons/cssbuy.svg new file mode 100644 index 0000000000..62aeeb7eed --- /dev/null +++ b/auth/assets/custom-icons/icons/cssbuy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/finary.svg b/auth/assets/custom-icons/icons/finary.svg new file mode 100644 index 0000000000..3e553974f5 --- /dev/null +++ b/auth/assets/custom-icons/icons/finary.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/fzj.svg b/auth/assets/custom-icons/icons/fzj.svg new file mode 100644 index 0000000000..c2fb55c7c7 --- /dev/null +++ b/auth/assets/custom-icons/icons/fzj.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/auth/assets/custom-icons/icons/gerid.svg b/auth/assets/custom-icons/icons/gerid.svg new file mode 100644 index 0000000000..c1f9593174 --- /dev/null +++ b/auth/assets/custom-icons/icons/gerid.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/auth/assets/custom-icons/icons/mercado_livre.svg b/auth/assets/custom-icons/icons/mercado_libre.svg similarity index 100% rename from auth/assets/custom-icons/icons/mercado_livre.svg rename to auth/assets/custom-icons/icons/mercado_libre.svg diff --git a/auth/assets/custom-icons/icons/mistral.svg b/auth/assets/custom-icons/icons/mistral.svg index 1a6cd7fc03..3885997f7c 100644 --- a/auth/assets/custom-icons/icons/mistral.svg +++ b/auth/assets/custom-icons/icons/mistral.svg @@ -1,15 +1,7 @@ - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/oracle_cloud.svg b/auth/assets/custom-icons/icons/oracle_cloud.svg new file mode 100644 index 0000000000..d7a2dbc5b7 --- /dev/null +++ b/auth/assets/custom-icons/icons/oracle_cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/r10.svg b/auth/assets/custom-icons/icons/r10.svg new file mode 100644 index 0000000000..5be8bf4800 --- /dev/null +++ b/auth/assets/custom-icons/icons/r10.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/tebex.svg b/auth/assets/custom-icons/icons/tebex.svg new file mode 100644 index 0000000000..b7ec26fae5 --- /dev/null +++ b/auth/assets/custom-icons/icons/tebex.svg @@ -0,0 +1,19 @@ + + vikunja + + + + + + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/toshl_finance.svg b/auth/assets/custom-icons/icons/toshl_finance.svg new file mode 100644 index 0000000000..64e90dc34c --- /dev/null +++ b/auth/assets/custom-icons/icons/toshl_finance.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/uollet.svg b/auth/assets/custom-icons/icons/uollet.svg new file mode 100644 index 0000000000..c767077239 --- /dev/null +++ b/auth/assets/custom-icons/icons/uollet.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/xai.svg b/auth/assets/custom-icons/icons/xai.svg new file mode 100644 index 0000000000..59246d5b24 --- /dev/null +++ b/auth/assets/custom-icons/icons/xai.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/auth/lib/core/configuration.dart b/auth/lib/core/configuration.dart index 7264ff6863..7ba6fa4922 100644 --- a/auth/lib/core/configuration.dart +++ b/auth/lib/core/configuration.dart @@ -14,7 +14,6 @@ import 'package:ente_auth/models/key_gen_result.dart'; import 'package:ente_auth/models/private_key_attributes.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/utils/directory_utils.dart'; -import 'package:ente_auth/utils/lock_screen_settings.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logging/logging.dart'; @@ -33,7 +32,6 @@ class Configuration { static const emailKey = "email"; static const keyAttributesKey = "key_attributes"; - static const keyShouldShowLockScreen = "should_show_lock_screen"; static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time"; static const keyKey = "key"; static const secretKeyKey = "secret_key"; @@ -133,7 +131,6 @@ class Configuration { key: key, ); } - await LockScreenSettings.instance.removePinAndPassword(); await AuthenticatorDB.instance.clearTable(); _key = null; _cachedToken = null; @@ -468,24 +465,6 @@ class Configuration { await _preferences.setBool(hasOptedForOfflineModeKey, true); } - Future shouldShowLockScreen() async { - final bool isPin = await LockScreenSettings.instance.isPinSet(); - final bool isPass = await LockScreenSettings.instance.isPasswordSet(); - return isPin || isPass || shouldShowSystemLockScreen(); - } - - bool shouldShowSystemLockScreen() { - if (_preferences.containsKey(keyShouldShowLockScreen)) { - return _preferences.getBool(keyShouldShowLockScreen)!; - } else { - return false; - } - } - - Future setSystemLockScreen(bool value) { - return _preferences.setBool(keyShouldShowLockScreen, value); - } - void setVolatilePassword(String volatilePassword) { _volatilePassword = volatilePassword; } diff --git a/auth/lib/core/constants.dart b/auth/lib/core/constants.dart index b58d7dce61..44af78c841 100644 --- a/auth/lib/core/constants.dart +++ b/auth/lib/core/constants.dart @@ -11,7 +11,7 @@ const String roadmapURL = "https://roadmap.ente.io"; const String kAccountsUrl = "https://accounts.ente.io"; const String githubFeatureRequestUrl = - "https://github.com/ente-io/ente/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+requests%22+label%3A%22-+auth%22+sort%3Atop"; + "https://github.com/ente-io/ente/discussions/categories/enhancements?discussions_q=is%3Aopen%+label%3A%22-+auth%22+sort%3Atop"; const int microSecondsInDay = 86400000000; const int android11SDKINT = 30; const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748 diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index 3f3c7c8974..1375146e48 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -73,7 +73,10 @@ class AuthenticatorGateway { ); } - Future> getDiff(int sinceTime, {int limit = 500}) async { + Future<(List, int?)> getDiff( + int sinceTime, { + int limit = 500, + }) async { try { final response = await _enteDio.get( "/authenticator/entity/diff", @@ -84,11 +87,12 @@ class AuthenticatorGateway { ); final List authEntities = []; final diff = response.data["diff"] as List; + final int? unixTimeInMicroSeconds = response.data["timestamp"] as int?; for (var entry in diff) { final AuthEntity entity = AuthEntity.fromMap(entry); authEntities.add(entity); } - return authEntities; + return (authEntities, unixTimeInMicroSeconds); } catch (e) { if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); diff --git a/auth/lib/l10n/arb/app_ar.arb b/auth/lib/l10n/arb/app_ar.arb index 7128f1b1e6..7872963dd8 100644 --- a/auth/lib/l10n/arb/app_ar.arb +++ b/auth/lib/l10n/arb/app_ar.arb @@ -47,7 +47,7 @@ "saveAction": "حفظ", "nextTotpTitle": "التالي", "deleteCodeTitle": "حذف الرمز؟", - "deleteCodeMessage": "هل أنت متأكد من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.", + "deleteCodeMessage": "هل أنت متيقِّن من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.", "trashCode": "حذف الكود؟", "trashCodeMessage": "هل أنت متيقِّن أنك تريد حذف الكود الخاص بـ {account}؟", "trash": "سلة المهملات", @@ -173,6 +173,7 @@ "invalidQRCode": "شيفرة استجابة سريعة غير صالحة", "noRecoveryKeyTitle": "لا يوجد مفتاح استرجاع؟", "enterEmailHint": "أدخل عنوان البريد الإلكتروني الخاص بك", + "enterNewEmailHint": "أدخل عنوان بريدك الإلكتروني الجديد", "invalidEmailTitle": "عنوان البريد الإلكتروني غير صالح", "invalidEmailMessage": "الرجاء إدخال بريد إلكتروني صالح.", "deleteAccount": "إزالة الحساب", @@ -513,5 +514,10 @@ "free5GB": "5GB مجانًا على ente صور", "loginWithAuthAccount": "سجّل الدخول باستخدام حساب المُصادقة", "freeStorageOffer": "خَصْم 10٪ على صور ente", - "freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى" + "freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى", + "advanced": "متقدم", + "algorithm": "الخوارزمية", + "type": "النوع", + "period": "المدّة", + "digits": "الأرقام" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_be.arb b/auth/lib/l10n/arb/app_be.arb index 1f0cf8b089..97b864e5f1 100644 --- a/auth/lib/l10n/arb/app_be.arb +++ b/auth/lib/l10n/arb/app_be.arb @@ -8,7 +8,7 @@ }, "onBoardingBody": "Бяспечна зрабіць рэзервовую копію кодаў 2ФА", "onBoardingGetStarted": "Пачаць", - "setupFirstAccount": "Наладзіць ваш першы ўліковы запіс", + "setupFirstAccount": "Наладзіць свой першы ўліковы запіс", "importScanQrCode": "Сканіраваць код QR-код", "qrCode": "QR-код", "importEnterSetupKey": "Увесці ключ наладжвання", @@ -45,19 +45,38 @@ "timeBasedKeyType": "Заснаваныя на часе (TOTP)", "counterBasedKeyType": "Заснаваныя на лічыльніку (HOTP)", "saveAction": "Захаваць", - "nextTotpTitle": "наступны", + "nextTotpTitle": "далей", "deleteCodeTitle": "Выдаліць код?", "deleteCodeMessage": "Вы сапраўды хочаце выдаліць гэты код? Гэта дзеянне з'яўляецца незваротным.", "trashCode": "Выдаліць код?", "trashCodeMessage": "Вы сапраўды хочаце выдаліць код для {account}?", "trash": "Сметніца", - "viewLogsAction": "Паглядзець журнал", - "preparingLogsTitle": "Падрыхтоўка журнала...", + "viewLogsAction": "Паглядзець журналы", + "preparingLogsTitle": "Падрыхтоўка журналаў...", "emailLogsTitle": "Адправіць журнал па электроннай пошце", - "exportLogsAction": "Экспартаваць журнал", - "reportABug": "Паведаміць пра памылку", - "reportBug": "Паведаміць пра памылку", + "emailLogsMessage": "Адпраўце журналы на {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "Скапіяваць электронную пошту", + "exportLogsAction": "Экспартаваць журналы", + "reportABug": "Паведаміць аб памылцы", + "crashAndErrorReporting": "Справаздачы аб збоях і памылках", + "reportBug": "Паведаміць аб памылцы", + "emailUsMessage": "Адпраўце нам ліст на {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, "contactSupport": "Звярнуцца ў службу падтрымкі", + "rateUsOnStore": "Ацаніць нас у {storeName}", "blog": "Блог", "verifyPassword": "Праверыць пароль", "pleaseWait": "Пачакайце...", @@ -66,32 +85,128 @@ "useRecoveryKey": "Выкарыстоўваць ключ аднаўлення", "incorrectPasswordTitle": "Няправільны пароль", "welcomeBack": "З вяртаннем!", + "changeEmail": "Змяніць адрас электроннай пошты", "changePassword": "Змяніць пароль", "data": "Даныя", "importCodes": "Імпартаваць коды", + "importTypePlainText": "Звычайны тэкст", + "importTypeEnteEncrypted": "Шыфраванне экспартавання з Ente", + "passwordForDecryptingExport": "Пароль для дэшыфроўкі экспартавання", "passwordEmptyError": "Пароль не можа быць пустым", "importFromApp": "Імпартаваць коды з {appName}", "exportCodes": "Экспартаваць коды", "importLabel": "Імпарт", "selectFile": "Выбраць файл", + "ok": "OK", + "cancel": "Скасаваць", "yes": "Так", "no": "Не", "email": "Электронная пошта", "support": "Падтрымка", + "general": "Агульныя", "settings": "Налады", + "copied": "Скапіявана", "pleaseTryAgain": "Калі ласка, паспрабуйце яшчэ раз", "delete": "Выдаліць", "enterYourPasswordHint": "Увядзіце ваш пароль", + "forgotPassword": "Забылі пароль", + "oops": "Вой", "faq": "Частыя пытанні", + "scan": "Сканіраваць", + "verify": "Праверыць", + "verifyPasskey": "Праверыць ключ доступу", + "loginWithTOTP": "Увайсці з TOTP", + "recoverAccount": "Аднавіць уліковы запіс", + "recover": "Аднавіць", + "invalidQRCode": "Памылковы QR-код", "deleteAccount": "Выдаліць уліковы запіс", "noDeleteAccountAction": "Не, выдаліць уліковы запіс", "sendEmail": "Адправіць ліст", "createNewAccount": "Стварыць новы ўліковы запіс", + "weakStrength": "Ненадзейны", + "strongStrength": "Надзейны", + "moderateStrength": "Умераная", "confirmPassword": "Пацвердзіць пароль", "close": "Закрыць", "oopsSomethingWentWrong": "Штосьці пайшло не так.", + "selectLanguage": "Выберыце мову", "language": "Мова", + "social": "Сацыяльныя сеткі", "security": "Бяспека", "searchHint": "Пошук...", - "search": "Пошук" + "search": "Пошук", + "addCode": "Дадаць код", + "scanAQrCode": "Сканіраваць QR-код", + "edit": "Рэдагаваць", + "share": "Абагуліць", + "restore": "Аднавіць", + "error": "Памылка", + "saveKey": "Захаваць ключ", + "save": "Захаваць", + "send": "Адправіць", + "back": "Назад", + "createAccount": "Стварыць уліковы запіс", + "password": "Пароль", + "privacyPolicyTitle": "Палітыка прыватнасці", + "termsOfServicesTitle": "Умовы", + "changePasswordTitle": "Змяніць пароль", + "resetPasswordTitle": "Скінуць пароль", + "encryptionKeys": "Ключы шыфравання", + "continueLabel": "Працягнуць", + "insecureDevice": "Небяспечная прылада", + "logInLabel": "Увайсці", + "logout": "Выйсці", + "exit": "Выхад", + "theme": "Тема", + "lightTheme": "Светлая", + "darkTheme": "Цёмная", + "systemTheme": "Сістэманая", + "confirm": "Пацвердзіць", + "emailYourLogs": "Адправіць журналы", + "about": "Аб праграме", + "privacy": "Прыватнасць", + "terms": "Умовы", + "downloadUpdate": "Спампаваць", + "update": "Абнавіць", + "warning": "Папярэджанне", + "importSuccessTitle": "Ура!", + "sorry": "Прабачце", + "pendingSyncs": "Папярэджанне", + "manualSort": "Карыстальніцкая", + "incorrectCode": "Няправільны код", + "enterPassword": "Увядзіце пароль", + "export": "Экспартаваць", + "singIn": "Увайсці", + "androidBiometricSuccess": "Паспяхова", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, + "androidCancelButton": "Скасаваць", + "@androidCancelButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." + }, + "goToSettings": "Перайсці ў налады", + "@goToSettings": { + "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." + }, + "iOSOkButton": "OK", + "@iOSOkButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." + }, + "passkey": "Ключ доступу", + "pinText": "Замацаваць", + "unpinText": "Адмацаваць", + "pinned": "Замацавана", + "tags": "Тэгі", + "createNewTag": "Стварыць новы тэг", + "tag": "Тэг", + "create": "Стварыць", + "viewRawCodes": "Паглядзець неапрацаваныя коды", + "rawCodeData": "Неапрацаваныя даныя кода", + "appLock": "Блакіроўка праграмы", + "next": "Далей", + "tapToUnlock": "Націсніце для разблакіроўкі", + "type": "Тып", + "period": "Перыяд", + "digits": "Лічбы" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_cs.arb b/auth/lib/l10n/arb/app_cs.arb index 5dfb19cc33..75041c5f07 100644 --- a/auth/lib/l10n/arb/app_cs.arb +++ b/auth/lib/l10n/arb/app_cs.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Použít obnovovací klíč", "incorrectPasswordTitle": "Nesprávné heslo", "welcomeBack": "Vítejte zpět!", + "emailAlreadyRegistered": "E-mail je již registrován.", + "emailNotRegistered": "E-mail není registrován.", "madeWithLoveAtPrefix": "vyrobeno s ❤️ v ", "supportDevs": "Předplaťte si ente, abyste nás podpořili", "supportDiscount": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok", @@ -495,9 +497,18 @@ "appLockOfflineModeWarning": "Zvolili jste si pokračování bez zálohování. Pokud zapomenete heslo do aplikace, přístup k datům bude uzamčen.", "duplicateCodes": "Duplikovat kódy", "noDuplicates": "✨ Žádné duplikáty", + "youveNoDuplicateCodesThatCanBeCleared": "Nemáte žádné duplicitní kódy k odstranění", "deduplicateCodes": "Deduplikovat kódy", "deselectAll": "Zrušit výběr všech položek", "selectAll": "Vybrat vše", "deleteDuplicates": "Odstranit duplikáty", - "plainHTML": "Prosté HTML" + "plainHTML": "Prosté HTML", + "tellUsWhatYouThink": "Sdělte nám svůj názor", + "dropReviewiOS": "Zanechat recenzi na App Store", + "dropReviewAndroid": "Zanechat recenzi na Play Store", + "supportEnte": "Podpořte ente", + "giveUsAStarOnGithub": "Dejte nám hvězdu na Githubu", + "free5GB": "5GB zdarma na ente Fotky", + "freeStorageOffer": "10% sleva na ente fotky", + "freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 63b9870f01..5e6ff47f98 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -91,39 +91,39 @@ "emailAlreadyRegistered": "E-Mail ist bereits registriert.", "emailNotRegistered": "E-Mail-Adresse nicht registriert.", "madeWithLoveAtPrefix": "gemacht mit ❤️ bei ", - "supportDevs": "Bei ente registrieren, um das Projekt zu unterstützen", + "supportDevs": "Abonnieren Sie ente, um das Projekt zu unterstützen", "supportDiscount": "Benutzen Sie den Rabattcode \"AUTH\" für 10 % Rabatt im ersten Jahr", "changeEmail": "E-Mail ändern", "changePassword": "Passwort ändern", - "data": "Datei", + "data": "Daten", "importCodes": "Codes importieren", "importTypePlainText": "Klartext", "importTypeEnteEncrypted": "Verschlüsselter Ente-Export", "passwordForDecryptingExport": "Passwort um den Export zu entschlüsseln", "passwordEmptyError": "Passwort kann nicht leer sein", - "importFromApp": "Importiere Codes von {appName}", - "importGoogleAuthGuide": "Exportiere deine Accounts von Google Authenticator zu einem QR-Code, durch die \"Konten übertragen\" Option. Scanne den QR-Code danach mit einem anderen Gerät.\n\nTipp: Du kannst die Kamera eines Laptops verwenden, um ein Foto den dem QR-Code zu erstellen.", - "importSelectJsonFile": "Wähle eine JSON-Datei", + "importFromApp": "Importieren Sie Codes von {appName}", + "importGoogleAuthGuide": "Exportieren Sie Ihre Accounts von Google Authenticator zu einem QR-Code, mithilfe der \"Konten übertragen\" Option. Scanne den QR-Code danach mit einem anderen Gerät.\n\nTipp: Sie können die Kamera eines Laptops verwenden, um ein Foto vom QR-Code zu erstellen.", + "importSelectJsonFile": "Wählen Sie eine JSON-Datei", "importSelectAppExport": "{appName} Exportdatei auswählen", - "importEnteEncGuide": "Wähle die von Ente exportierte, verschlüsselte JSON-Datei", - "importRaivoGuide": "Verwenden Sie die Option \"Export OTPs to Zip archive\" in den Raivo-Einstellungen.\n\nEntpacken Sie die Zip-Datei und importieren Sie die JSON-Datei.", + "importEnteEncGuide": "Wählen Sie die von Ente exportierte, verschlüsselte JSON-Datei", + "importRaivoGuide": "Verwenden Sie die Option \"Export OTPs to Zip archive\" in den Einstellungen von Raivo.\n\nEntpacken Sie die Zip-Datei und importieren Sie die JSON-Datei.", "importBitwardenGuide": "Verwenden Sie die Option \"Tresor exportieren\" innerhalb der Bitwarden Tools und importieren Sie die unverschlüsselte JSON-Datei.", - "importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Aegis-Einstellungen.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.", + "importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Einstellungen von Aegis.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.", "import2FasGuide": "Verwenden Sie unter \"Einstellungen → Backup\" die Option \"Exportieren\" in 2FAS.\n\nFalls Ihr Backup verschlüsselt ist, müssen Sie das Passwort eingeben, um das Backup zu entschlüsseln.", "importLastpassGuide": "Verwenden Sie die Option \"Konten übertragen → Konten in Datei exportieren\" in den Lastpass Authenticator Einstellungen. \nImportieren Sie anschließend die heruntergeladene JSON-Datei.", "exportCodes": "Codes exportieren", "importLabel": "Importieren", - "importInstruction": "Bitte wählen sie eine Datei die Codes in folgendem Format beinhaltet", - "importCodeDelimiterInfo": "Codes können in einer neuen Zeile stehen oder durch Kommata getrennt sein", + "importInstruction": "Bitte wählen Sie eine Datei die Codes in folgendem Format beinhaltet", + "importCodeDelimiterInfo": "Codes können in einer neuen Zeile stehen oder durch ein Komma getrennt sein", "selectFile": "Datei auswählen", "emailVerificationToggle": "E-Mail-Verifizierung", - "emailVerificationEnableWarning": "Stellen Sie sicher, eine Kopie Ihrer Zwei-Faktor-Authentifizierung an anderer Stelle zu speichern, um zu vermeiden, dass Sie sich versehentlich aus Ihrem Account aussperren.", - "authToChangeEmailVerificationSetting": "Bitte Authentifizieren um die E-Mail Bestätigung zu ändern", + "emailVerificationEnableWarning": "Um zu vermeiden, versehentlich aus Ihrem Konto ausgesperrt zu werden, stellen Sie sicher, dass Sie den Zwei-Faktor-Authentifizierungscode für Ihr E-Mail-Konto außerhalb von Ente Auth speichern, bevor Sie die E-Mail-Verifizierung aktivieren.", + "authToChangeEmailVerificationSetting": "Bitte authentifizieren, um die E-Mail Bestätigung zu ändern", "authenticateGeneric": "Bitte authentifizieren", - "authToViewYourRecoveryKey": "Bitte authentifizieren um ihren Wiederherstellungscode anzuzeigen", - "authToChangeYourEmail": "Bitte authentifizieren um ihre Emailadresse zu ändern", - "authToChangeYourPassword": "Bitte authentifizieren um ihr Passwort zu ändern", - "authToViewSecrets": "Bitte authentifizieren Sie sich, um ihren Wiederherstellungscode anzuzeigen", + "authToViewYourRecoveryKey": "Bitte authentifizieren, um Ihren Wiederherstellungscode anzuzeigen", + "authToChangeYourEmail": "Bitte authentifizieren, um Ihre E-Mail-Adresse zu ändern", + "authToChangeYourPassword": "Bitte authentifizieren, um Ihr Passwort zu ändern", + "authToViewSecrets": "Bitte authentifizieren, um Ihren Wiederherstellungscode anzuzeigen", "authToInitiateSignIn": "Bitte authentifizieren, um die Anmeldung zum Backup zu starten.", "ok": "Ok", "cancel": "Abbrechen", @@ -140,18 +140,18 @@ "delete": "Löschen", "enterYourPasswordHint": "Geben Sie Ihr Passwort ein", "forgotPassword": "Passwort vergessen", - "oops": "Hopla", + "oops": "Hoppla", "suggestFeatures": "Features vorschlagen", "faq": "FAQ", "somethingWentWrongMessage": "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut", "leaveFamily": "Familie verlassen", "leaveFamilyMessage": "Sind Sie sicher, dass Sie den Familien-Plan verlassen wollen?", "inFamilyPlanMessage": "Sie haben einen Familien-Plan!", - "hintForMobile": "Lange drücken, um den Code zu bearbeiten oder zu entfernen.", + "hintForMobile": "Lange auf einen Code drücken, um ihn zu bearbeiten oder zu entfernen.", "hintForDesktop": "Klicken Sie mit der rechten Maustaste auf einen Code zum Bearbeiten oder Entfernen.", "scan": "Scannen", "scanACode": "Scan einen Code", - "verify": "Überprüfen Sie", + "verify": "Verifizieren", "verifyEmail": "E-Mail-Adresse verifizieren", "enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.", "lostDeviceTitle": "Gerät verloren?", @@ -172,9 +172,10 @@ }, "invalidQRCode": "Ungültiger QR-Code", "noRecoveryKeyTitle": "Kein Wiederherstellungsschlüssel?", - "enterEmailHint": "Geben Sie Ihre E-Mail Adresse ein", - "invalidEmailTitle": "Ungültige E-Mail Adresse", - "invalidEmailMessage": "Bitte geben Sie eine gültige E-Mail Adresse ein.", + "enterEmailHint": "Geben Sie Ihre E-Mail-Adresse ein", + "enterNewEmailHint": "Gib deine neue E-Mail-Adresse ein", + "invalidEmailTitle": "Ungültige E-Mail-Adresse", + "invalidEmailMessage": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", "deleteAccount": "Konto löschen", "deleteAccountQuery": "Es tut uns leid, dass Sie gehen. Haben Sie ein Problem?", "yesSendFeedbackAction": "Ja, Feedback senden", @@ -187,7 +188,7 @@ "moderateStrength": "Mittel", "confirmPassword": "Bestätigen Sie das Passwort", "close": "Schließen", - "oopsSomethingWentWrong": "Ups, da ist etwas schief gelaufen.", + "oopsSomethingWentWrong": "Hoppla, da ist etwas schiefgelaufen.", "selectLanguage": "Sprache auswählen", "language": "Sprache", "social": "Social", @@ -203,26 +204,26 @@ "noResult": "Kein Ergebnis", "addCode": "Code hinzufügen", "scanAQrCode": "QR-Code scannen", - "enterDetailsManually": "Details manuell hinzufügen", + "enterDetailsManually": "Daten manuell hinzufügen", "edit": "Editieren", "share": "Teilen", "shareCodes": "Codes teilen", "shareCodesDuration": "Wählen Sie die Dauer aus, für die Sie die Codes teilen möchten.", "restore": "Wiederherstellen", - "copiedToClipboard": "In die Zwischenablage kopieren", - "copiedNextToClipboard": "Nächster Code wurde in die Zwischenablage kopiert", + "copiedToClipboard": "In die Zwischenablage kopiert", + "copiedNextToClipboard": "Nächster Code in die Zwischenablage kopiert", "error": "Fehler", "recoveryKeyCopiedToClipboard": "Wiederherstellungsschlüssel in die Zwischenablage kopiert", "recoveryKeyOnForgotPassword": "Sollten sie ihr Passwort vergessen, dann ist dieser Schlüssel die einzige Möglichkeit ihre Daten wiederherzustellen.", - "recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern sie dieses diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.", + "recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern Sie diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.", "doThisLater": "Auf später verschieben", "saveKey": "Schlüssel speichern", "save": "Speichern", "send": "Senden", - "saveOrSendDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) oder an andere Apps senden?", + "saveOrSendDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) speichern oder an andere Apps senden?", "saveOnlyDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) speichern?", "back": "Zurück", - "createAccount": "Account erstellen", + "createAccount": "Konto erstellen", "passwordStrength": "Passwortstärke: {passwordStrengthValue}", "@passwordStrength": { "description": "Text to indicate the password strength", @@ -244,17 +245,17 @@ "changePasswordTitle": "Passwort ändern", "resetPasswordTitle": "Passwort zurücksetzen", "encryptionKeys": "Verschlüsselungsschlüssel", - "passwordWarning": "Wir speichern dieses Passwort nicht. Wenn du es vergisst, können wir deine Daten nicht entschlüsseln", - "enterPasswordToEncrypt": "Gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", - "enterNewPasswordToEncrypt": "Gib ein neues Passwort ein, mit dem wir deine Daten verschlüsseln können", + "passwordWarning": "Wir speichern dieses Passwort nicht. Wenn Sie es vergessen, können wir Ihre Daten nicht entschlüsseln", + "enterPasswordToEncrypt": "Geben Sie ein Passwort ein, mit dem wir Ihre Daten verschlüsseln können", + "enterNewPasswordToEncrypt": "Geben Sie ein neues Passwort ein, mit dem wir Ihre Daten verschlüsseln können", "passwordChangedSuccessfully": "Passwort erfolgreich geändert", "generatingEncryptionKeys": "Generierung von Verschlüsselungsschlüsseln...", "continueLabel": "Weiter", "insecureDevice": "Unsicheres Gerät", "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Es tut uns leid, wir konnten keine sicheren Schlüssel auf diesem Gerät generieren.\n\nBitte registrieren Sie sich auf einem anderen Gerät.", "howItWorks": "So funktioniert's", - "ackPasswordLostWarning": "Ich verstehe, dass der Verlust meines Passworts zum Verlust meiner Daten führen kann, denn diese ist Ende-zu-Ende verschlüsselt.", - "loginTerms": "Durch das Klicken auf den Login-Button, stimme ich den Nutzungsbedingungen und den Datenschutzbestimmungen zu", + "ackPasswordLostWarning": "Ich verstehe, dass der Verlust meines Passworts zum Verlust meiner Daten führen kann, denn diese sind Ende-zu-Ende verschlüsselt.", + "loginTerms": "Durch das Klicken auf den Login-Button, stimme ich den Nutzungsbedingungen und den Datenschutzbestimmungen zu", "logInLabel": "Einloggen", "logout": "Ausloggen", "areYouSureYouWantToLogout": "Sind sie sicher, dass sie sich ausloggen möchten?", @@ -266,7 +267,7 @@ "systemTheme": "System", "verifyingRecoveryKey": "Verifiziere Wiederherstellungsschlüssel...", "recoveryKeyVerified": "Wiederherstellungsschlüssel verifiziert", - "recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken sie daran, dass sie ihren Wiederherstellungsschlüssel sicher aufbewahren.", + "recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken Sie daran, den Wiederherstellungsschlüssel sicher aufzubewahren.", "invalidRecoveryKey": "Der eingegebene Wiederherstellungsschlüssel ist nicht gültig. Bitte stellen sie sicher, dass er aus 24 Wörtern besteht und prüfen sie die Schreibweise eines jeden.\n\nSollten sie einen Wiederherstellungsschlüssel im alten Format eingegeben haben vergewissern sie sich, dass er 64 Zeichen lang ist und prüfen sie jedes dieser Zeichen.", "recreatePasswordTitle": "Neues Passwort erstellen", "recreatePasswordBody": "Das benutzte Gerät ist nicht leistungsfähig genug das Passwort zu prüfen. Wir können es aber neu erstellen damit es auf jedem Gerät funktioniert. \n\nBitte loggen sie sich mit ihrem Wiederherstellungsschlüssel ein und erstellen sie ein neues Passwort (Sie können das selbe Passwort wieder verwenden, wenn sie möchten).", @@ -277,15 +278,15 @@ "recoveryKeyVerifyReason": "Ihr Wiederherstellungsschlüssel ist der einzige Weg ihre Fotos wiederherzustellen sollten, sie ihr Passwort vergessen. Sie finden ihren Wiederherstellungsschlüssel unter Einstellungen > Account.\n\nBitte tragen sie ihren Wiederherstellungsschlüssel hier ein um zu prüfen ob sie in korrekt abgespeichert haben.", "confirmYourRecoveryKey": "Wiederherstellungsschlüssel bestätigen", "confirm": "Bestätigen", - "emailYourLogs": "Email mit Logs senden", + "emailYourLogs": "E-Mail mit Logs senden", "pleaseSendTheLogsTo": "Bitte Logs an {toEmail} senden", - "copyEmailAddress": "Emailadresse kopieren", + "copyEmailAddress": "E-Mail-Adresse kopieren", "exportLogs": "Logs exportieren", "enterYourRecoveryKey": "Wiederherstellungsschlüssel eingeben", - "tempErrorContactSupportIfPersists": "Etwas ist schiefgelaufen. Bitte versuchen sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.", + "tempErrorContactSupportIfPersists": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.", "networkHostLookUpErr": "Ente ist im Moment nicht erreichbar. Bitte überprüfen Sie Ihre Netzwerkeinstellungen. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.", "networkConnectionRefusedErr": "Ente ist im Moment nicht erreichbar. Bitte versuchen Sie es später erneut. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.", - "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuchen sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren Sie unser Supportteam.", "about": "Über uns", "weAreOpenSource": "Wir sind Opensource!", "privacy": "Datenschutz", @@ -316,10 +317,10 @@ } } }, - "sorry": "Entschuldigen sie", - "importFailureDesc": "Ausgewählte Datei ließ sich nicht verarbeiten.\nBitte wenden sie sich an support@ente.io für Hilfe!", + "sorry": "Entschuldigen Sie", + "importFailureDesc": "Ausgewählte Datei ließ sich nicht verarbeiten.\nBitte wenden Sie sich an support@ente.io für Hilfe!", "pendingSyncs": "Warnung", - "pendingSyncsWarningBody": "Einige Codes wurden nicht gesichert.\n\nBitte gehen sie sicher, dass sie einen Backupcode für diese Codes haben bevor sie sich ausloggen.", + "pendingSyncsWarningBody": "Einige Codes wurden nicht gesichert.\n\nBitte gehen Sie sicher, dass Sie einen Backupcode für diese Codes haben bevor Sie sich ausloggen.", "checkInboxAndSpamFolder": "Bitte überprüfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen", "tapToEnterCode": "Antippen, um den Code einzugeben", "resendEmail": "E-Mail erneut senden", @@ -339,10 +340,10 @@ "mostFrequentlyUsed": "Häufig verwendet", "mostRecentlyUsed": "Zuletzt verwendet", "activeSessions": "Aktive Sitzungen", - "somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte versuche es erneut", - "thisWillLogYouOutOfThisDevice": "Dadurch wirst du von diesem Gerät abgemeldet!", - "thisWillLogYouOutOfTheFollowingDevice": "Dadurch wirst du von folgendem Gerät abgemeldet:", - "terminateSession": "Sitzungen beenden?", + "somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte erneut versuchen", + "thisWillLogYouOutOfThisDevice": "Dadurch werden Sie von diesem Gerät abgemeldet!", + "thisWillLogYouOutOfTheFollowingDevice": "Dadurch werden Sie vom folgendem Gerät abgemeldet:", + "terminateSession": "Sitzung beenden?", "terminate": "Beenden", "thisDevice": "Dieses Gerät", "toResetVerifyEmail": "Um Ihr Passwort zurückzusetzen, verifizieren Sie bitte zuerst Ihre E-Mail-Adresse.", @@ -352,7 +353,7 @@ "incorrectCode": "Falscher Code", "sorryTheCodeYouveEnteredIsIncorrect": "Leider ist der eingegebene Code falsch", "emailChangedTo": "E-Mail-Adresse geändert zu {newEmail}", - "authenticationFailedPleaseTryAgain": "Authentifizierung fehlgeschlagen, versuchen Sie es bitte erneut", + "authenticationFailedPleaseTryAgain": "Authentifizierung fehlgeschlagen, bitte erneut versuchen", "authenticationSuccessful": "Authentifizierung erfolgreich!", "twofactorAuthenticationSuccessfullyReset": "Zwei-Faktor-Authentifizierung (2FA) erfolgreich zurückgesetzt", "incorrectRecoveryKey": "Falscher Wiederherstellungs-Schlüssel", @@ -365,22 +366,22 @@ "passwordToEncryptExport": "Passwort zum Verschlüssen des Exports", "export": "Export", "useOffline": "Ohne Backup verwenden", - "signInToBackup": "Melde dich an, um deine Codes zu sichern", + "signInToBackup": "Melden Sie sich an, um Ihre Codes zu sichern", "singIn": "Anmelden", - "sigInBackupReminder": "Bitte exportieren Sie Ihre Codes, um sicherzustellen, dass Sie ein Backup haben, aus dem Sie wiederherstellen können.", + "sigInBackupReminder": "Bitte exportieren Sie Ihre Codes, um sicherzustellen, dass Sie ein Backup haben, das Sie wiederherstellen können.", "offlineModeWarning": "Sie haben sich dafür entschieden, ohne Sicherungen fortzufahren. Bitte führen Sie manuelle Sicherungen durch, um sicherzustellen, dass Ihre Codes sicher sind.", "showLargeIcons": "Große Symbole anzeigen", "compactMode": "Kompaktmodus", "shouldHideCode": "Codes ausblenden", "doubleTapToViewHiddenCode": "Sie können auf einen Eintrag doppelt tippen, um den Code anzuzeigen", - "focusOnSearchBar": "Suche bei App-Start automatisch öffnen", - "confirmUpdatingkey": "Sind Sie sich sicher, dass Sie den Secret Key bearbeiten wollen?", + "focusOnSearchBar": "Suche beim App-Start fokussieren", + "confirmUpdatingkey": "Sind Sie sich sicher, dass Sie den geheimen Schlüssel bearbeiten wollen?", "minimizeAppOnCopy": "Beim Kopieren App minimieren", "editCodeAuthMessage": "Authentifizieren, um Code zu bearbeiten", "deleteCodeAuthMessage": "Authentifizieren, um Code zu löschen", "showQRAuthMessage": "Authentifizieren, um QR-Code anzuzeigen", "confirmAccountDeleteTitle": "Kontolöschung bestätigen", - "confirmAccountDeleteMessage": "Dieses Konto ist mit anderen Ente-Apps verknüpft, falls du welche verwendest.\n\nDeine hochgeladenen Daten werden in allen Ente-Apps zur Löschung vorgemerkt und dein Konto wird endgültig gelöscht.", + "confirmAccountDeleteMessage": "Dieses Konto ist mit anderen Ente-Apps verknüpft, falls Sie welche verwenden.\n\nIhre hochgeladenen Daten werden in allen Ente-Apps zur Löschung vorgemerkt und Ihr Konto wird endgültig gelöscht.", "androidBiometricHint": "Identität bestätigen", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -417,7 +418,7 @@ "@goToSettings": { "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." }, - "androidGoToSettingsDescription": "Auf Ihrem Gerät ist keine biometrische Authentifizierung eingerichtet. Gehen Sie „Einstellungen“ > „Sicherheit“, um die biometrische Authentifizierung hinzuzufügen.", + "androidGoToSettingsDescription": "Auf Ihrem Gerät ist keine biometrische Authentifizierung eingerichtet. Gehen Sie zu 'Einstellungen > Sicherheit', um die biometrische Authentifizierung hinzuzufügen.", "@androidGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." }, @@ -434,9 +435,9 @@ "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, "noInternetConnection": "Keine Internetverbindung", - "pleaseCheckYourInternetConnectionAndTryAgain": "Bitte überprüfe deine Internetverbindung und versuche es erneut.", + "pleaseCheckYourInternetConnectionAndTryAgain": "Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie erneut.", "signOutFromOtherDevices": "Von anderen Geräten abmelden", - "signOutOtherBody": "Falls du denkst, dass jemand dein Passwort kennen könnte, kannst du alle anderen Geräte von deinem Account abmelden.", + "signOutOtherBody": "Falls Sie denken, dass jemand Ihr Passwort kennen könnte, können Sie alle anderen Geräte forcieren, sich von Ihrem Konto abzumelden.", "signOutOtherDevices": "Andere Geräte abmelden", "doNotSignOut": "Nicht abmelden", "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)", @@ -458,7 +459,7 @@ "pinText": "Anpinnen", "unpinText": "Lösen", "pinnedCodeMessage": "{code} wurde angepinnt", - "unpinnedCodeMessage": "{code} wird nicht weiter angepinnt", + "unpinnedCodeMessage": "{code} wurde losgelöst", "pinned": "Angeheftet", "tags": "Tags", "createNewTag": "Neuen Tag erstellen", @@ -474,7 +475,7 @@ "rawCodeData": "Rohcode Daten", "appLock": "App-Sperre", "noSystemLockFound": "Keine Systemsperre gefunden", - "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Um die App-Sperre zu aktivieren, konfiguriere bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen.", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Um die App-Sperre zu aktivieren, konfigurieren Sie bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen.", "autoLock": "Automatisches Sperren", "immediately": "Sofort", "reEnterPassword": "Passwort erneut eingeben", @@ -494,23 +495,29 @@ "setNewPin": "Neue PIN festlegen", "importFailureDescNew": "Die ausgewählte Datei konnte nicht verarbeitet werden.", "appLockNotEnabled": "App-Sperre nicht aktiviert", - "appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre", - "authToViewPasskey": "Bitte authentifizieren, um deinen Passkey zu sehen", - "duplicateCodes": "Doppelte Codes", + "appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Sicherheit > App-Sperre", + "authToViewPasskey": "Bitte authentifizieren, um Ihren Passkey zu sehen", + "appLockOfflineModeWarning": "Sie haben sich dazu entschieden, ohne Sicherungen fortzufahren. Wenn Sie Ihre App-Sperre vergessen, können Sie nicht mehr auf Ihre Daten zugreifen.", + "duplicateCodes": "Codes duplizieren", "noDuplicates": "✨ Keine Duplikate", "youveNoDuplicateCodesThatCanBeCleared": "Du hast keine doppelten Codes, die bereinigt werden können", "deduplicateCodes": "Codes deduplizieren", "deselectAll": "Alle abwählen", - "selectAll": "Alles auswählen", + "selectAll": "Alle auswählen", "deleteDuplicates": "Duplikate löschen", "plainHTML": "Reines HTML", "tellUsWhatYouThink": "Sagen Sie uns, was Sie denken", - "dropReviewiOS": "Hinterlasse eine Rezension im App Store", - "dropReviewAndroid": "Hinterlasse eine Rezension im Google Play Store", - "supportEnte": "Support ente", + "dropReviewiOS": "Hinterlassen Sie eine Rezension im App Store", + "dropReviewAndroid": "Hinterlassen Sie eine Rezension im Google Play Store", + "supportEnte": "Unterstütze ente", "giveUsAStarOnGithub": "Gib uns einen Stern auf Github", "free5GB": "5GB kostenlos auf ente Photos", "loginWithAuthAccount": "Mit Ihrem Auth Account anmelden", "freeStorageOffer": "10% Rabatt für ente Photos", - "freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im 1. Jahr zu sparen" + "freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im ersten Jahr zu sparen", + "advanced": "Erweitert", + "algorithm": "Algorithmus", + "type": "Typ", + "period": "Periode", + "digits": "Ziffern" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index 60ce6e41e5..6fc8f8ad55 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Invalid QR code", "noRecoveryKeyTitle": "No recovery key?", "enterEmailHint": "Enter your email address", + "enterNewEmailHint": "Enter your new email address", "invalidEmailTitle": "Invalid email address", "invalidEmailMessage": "Please enter a valid email address.", "deleteAccount": "Delete account", @@ -513,5 +514,10 @@ "free5GB": "5GB free on ente Photos", "loginWithAuthAccount": "Login with your Auth account", "freeStorageOffer": "10% off on ente photos", - "freeStorageOfferDescription": "Use code \"AUTH\" to get 10% off first year" + "freeStorageOfferDescription": "Use code \"AUTH\" to get 10% off first year", + "advanced": "Advanced", + "algorithm": "Algorithm", + "type": "Type", + "period": "Period", + "digits": "Digits" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_fr.arb b/auth/lib/l10n/arb/app_fr.arb index feb20b658b..4d0b461bed 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -173,6 +173,7 @@ "invalidQRCode": "QR code non valide", "noRecoveryKeyTitle": "Pas de clé de récupération ?", "enterEmailHint": "Entrez votre adresse e-mail", + "enterNewEmailHint": "Saisissez votre nouvelle adresse email", "invalidEmailTitle": "Adresse e-mail invalide", "invalidEmailMessage": "Veuillez saisir une adresse e-mail valide.", "deleteAccount": "Supprimer le compte", @@ -513,5 +514,10 @@ "free5GB": "5 Go gratuits sur Ente Photos", "loginWithAuthAccount": "Connectez-vous avec votre compte Auth", "freeStorageOffer": "10% de réduction sur Ente Photos", - "freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année" + "freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année", + "advanced": "Avancé", + "algorithm": "Algorithme", + "type": "Type", + "period": "Période", + "digits": "Chiffres" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_hu.arb b/auth/lib/l10n/arb/app_hu.arb index 8030313d85..68f8c804da 100644 --- a/auth/lib/l10n/arb/app_hu.arb +++ b/auth/lib/l10n/arb/app_hu.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Érvénytelen QR-kód", "noRecoveryKeyTitle": "Nincs helyreállítási kulcs?", "enterEmailHint": "Adja meg az e-mail címét", + "enterNewEmailHint": "Add meg az új e-mail címed", "invalidEmailTitle": "Érvénytelen e-mail cím", "invalidEmailMessage": "Kérjük, adjon meg egy érvényes e-mail címet.", "deleteAccount": "Fiók törlése", @@ -513,5 +514,6 @@ "free5GB": "5GB ingyen ente Photos", "loginWithAuthAccount": "Jelentkezzen be Auth fiókjával", "freeStorageOffer": "10% kedvezmény on ente photos", - "freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben" + "freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben", + "type": "Típus" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_id.arb b/auth/lib/l10n/arb/app_id.arb index c791a866a9..b5e2605a3f 100644 --- a/auth/lib/l10n/arb/app_id.arb +++ b/auth/lib/l10n/arb/app_id.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Gunakan kunci pemulihan", "incorrectPasswordTitle": "Kata sandi salah", "welcomeBack": "Selamat datang kembali!", + "emailAlreadyRegistered": "Email sudah terdaftar.", + "emailNotRegistered": "Email belum terdaftar.", "madeWithLoveAtPrefix": "dibuat dengan ❤️ di ", "supportDevs": "Berlangganan ente untuk mendukung kami", "supportDiscount": "Gunakan kode kupon \"AUTH\" untuk mendapatkan potongan 10% untuk tahun pertama", @@ -171,6 +173,7 @@ "invalidQRCode": "Kode QR tidak valid", "noRecoveryKeyTitle": "Tidak punya kunci pemulihan?", "enterEmailHint": "Masukkan alamat email Anda", + "enterNewEmailHint": "Masukkan alamat email baru anda", "invalidEmailTitle": "Alamat email tidak valid", "invalidEmailMessage": "Harap masukkan alamat email yang valid.", "deleteAccount": "Hapus akun", @@ -501,5 +504,12 @@ "deselectAll": "Batalkan semua pilihan", "selectAll": "Pilih semua", "deleteDuplicates": "Hapus duplikat", - "plainHTML": "HTML Sederhana" + "plainHTML": "HTML Sederhana", + "tellUsWhatYouThink": "Berikan pendapatmu", + "dropReviewAndroid": "Berikan ulasan di Play Store", + "advanced": "Lanjutan", + "algorithm": "Algoritma", + "type": "Tipe", + "period": "Periode", + "digits": "Digit" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index c19e3077da..ae4b79c7a9 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Codice QR non valido", "noRecoveryKeyTitle": "Nessuna chiave di recupero?", "enterEmailHint": "Inserisci il tuo indirizzo email", + "enterNewEmailHint": "Inserisci il tuo nuovo indirizzo email", "invalidEmailTitle": "Indirizzo email non valido", "invalidEmailMessage": "Inserisci un indirizzo email valido.", "deleteAccount": "Elimina account", @@ -513,5 +514,10 @@ "free5GB": "5GB gratis su ente Foto", "loginWithAuthAccount": "Accedi con il tuo account Auth", "freeStorageOffer": "10% di sconto su ente Foto", - "freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno" + "freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno", + "advanced": "Avanzate", + "algorithm": "Algoritmo", + "type": "Tipo", + "period": "Periodo", + "digits": "Cifre" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_lt.arb b/auth/lib/l10n/arb/app_lt.arb index babdd784ae..cd39c8b60f 100644 --- a/auth/lib/l10n/arb/app_lt.arb +++ b/auth/lib/l10n/arb/app_lt.arb @@ -508,9 +508,15 @@ "tellUsWhatYouThink": "Pasakykite mums, ką manote", "dropReviewiOS": "Rašyti apžvalgą parduotuvėje „App Store“", "dropReviewAndroid": "Rašyti apžvalgą parduotuvėje „Play“ parduotuvė“", + "supportEnte": "Paremti „ente“", "giveUsAStarOnGithub": "Suteikite mums žvaigždutę platformoje „Github“", "free5GB": "5 GB nemokami programai „ente“ nuotraukos", "loginWithAuthAccount": "Prisijungti su jūsų „Auth“ paskyra", "freeStorageOffer": "10 % nuolaida programai „ente“ nuotraukos", - "freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. " + "freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. ", + "advanced": "Išplėstiniai", + "algorithm": "Algoritmas", + "type": "Tipas", + "period": "Laikotarpis", + "digits": "Skaitmenys" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_nl.arb b/auth/lib/l10n/arb/app_nl.arb index 00ad14b4b5..125c0793b1 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Ongeldige QR-code", "noRecoveryKeyTitle": "Geen herstelsleutel?", "enterEmailHint": "Voer je e-mailadres in", + "enterNewEmailHint": "Voer uw nieuwe e-mailadres in", "invalidEmailTitle": "Ongeldig e-mailadres", "invalidEmailMessage": "Voer een geldig e-mailadres in.", "deleteAccount": "Account verwijderen", @@ -513,5 +514,10 @@ "free5GB": "5GB gratis op ente Photos", "loginWithAuthAccount": "Log in met je Auth account", "freeStorageOffer": "10% korting op ente photos", - "freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar" + "freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar", + "advanced": "Geavanceerd", + "algorithm": "Algoritme", + "type": "Type", + "period": "Periode", + "digits": "Cijfers" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index 51ad435732..a4177c8133 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Nieprawidłowy kod QR", "noRecoveryKeyTitle": "Brak klucza odzyskiwania?", "enterEmailHint": "Wprowadź adres e-mail", + "enterNewEmailHint": "Wprowadź nowy adres e-mail", "invalidEmailTitle": "Nieprawidłowy adres e-mail", "invalidEmailMessage": "Prosimy podać prawidłowy adres e-mail.", "deleteAccount": "Usuń konto", @@ -513,5 +514,10 @@ "free5GB": "5 GB za darmo na zdjęcia ente", "loginWithAuthAccount": "Zaloguj się przy użyciu konta Auth", "freeStorageOffer": "10% zniżki na zdjęcia ente", - "freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok" + "freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok", + "advanced": "Zaawansowane", + "algorithm": "Algorytm", + "type": "Rodzaj", + "period": "Okres", + "digits": "Cyfry" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index 2f542d5b82..0aff7e6cf8 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -173,6 +173,7 @@ "invalidQRCode": "QR Code inválido", "noRecoveryKeyTitle": "Sem chave de recuperação?", "enterEmailHint": "Insira o endereço de e-mail", + "enterNewEmailHint": "Insira seu novo e-mail", "invalidEmailTitle": "Endereço de e-mail inválido", "invalidEmailMessage": "Insira um endereço de e-mail válido.", "deleteAccount": "Excluir conta", @@ -513,5 +514,10 @@ "free5GB": "5GB grátis no ente Photos", "loginWithAuthAccount": "Registrar-se com sua conta Auth", "freeStorageOffer": "10% de desconto no ente photos", - "freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano" + "freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano", + "advanced": "Avançado", + "algorithm": "Algoritmo", + "type": "Tipo", + "period": "Período", + "digits": "Dígitos" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sr.arb b/auth/lib/l10n/arb/app_sr.arb new file mode 100644 index 0000000000..dc461d9e89 --- /dev/null +++ b/auth/lib/l10n/arb/app_sr.arb @@ -0,0 +1,523 @@ +{ + "account": "Налог", + "unlock": "Откључај", + "recoveryKey": "Резервни Кључ", + "counterAppBarTitle": "Бројач", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + }, + "onBoardingBody": "Сигурносно правити копију 2ФА кôдова", + "onBoardingGetStarted": "Почети", + "setupFirstAccount": "Подесити свој први налог", + "importScanQrCode": "Скенирај QR кôд", + "qrCode": "QR кôд", + "importEnterSetupKey": "Унети кључ за подешавање", + "importAccountPageTitle": "Унети детаље налога", + "secretCanNotBeEmpty": "Тајна не може бити празна", + "bothIssuerAndAccountCanNotBeEmpty": "И издавалац и рачун не могу бити празни", + "incorrectDetails": "Погрешни детаљи", + "pleaseVerifyDetails": "Проверите детаље и покушајте поново", + "codeIssuerHint": "Издавач", + "codeSecretKeyHint": "Тајни кључ", + "secret": "Тајна", + "all": "Све", + "notes": "Белешке", + "notesLengthLimit": "Белешке могу имати највише {count} знакова", + "@notesLengthLimit": { + "description": "Text to indicate the maximum number of characters allowed for notes", + "placeholders": { + "count": { + "description": "The maximum number of characters allowed for notes", + "type": "int", + "example": "100" + } + } + }, + "codeAccountHint": "Налог (you@domain.com)", + "codeTagHint": "Ознака", + "accountKeyType": "Тип кључа", + "sessionExpired": "Сесија је истекла", + "@sessionExpired": { + "description": "Title of the dialog when the users current session is invalid/expired" + }, + "pleaseLoginAgain": "Молимо да се поново пријавите", + "loggingOut": "Одјављивање...", + "timeBasedKeyType": "Временски (TOTP)", + "counterBasedKeyType": "На основу бројања (HOTP)", + "saveAction": "Сачувај", + "nextTotpTitle": "следеће", + "deleteCodeTitle": "Обрисати кôд?", + "deleteCodeMessage": "Сигурно желите да избришете овај кôд? Ова акција је неповратна.", + "trashCode": "Кôд у смеће?", + "trashCodeMessage": "Сигурно желите да поставите кôд у смеће за {account}?", + "trash": "Смеће", + "viewLogsAction": "Прегледај извештаје", + "sendLogsDescription": "Ово ће делите ваше записе како би нам помогли да вам исправимо проблем. Док преузмемо мере предострожности да осигурамо да осетљиве информације нису пријављене, охрабрујемо вас да прегледате ове записе пре него што их делите.", + "preparingLogsTitle": "Спремање извештаја...", + "emailLogsTitle": "Имејловати извештаје", + "emailLogsMessage": "Пошаљите извештаје на {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "Копирати имејл", + "exportLogsAction": "Извези изештаје", + "reportABug": "Пријави грешку", + "crashAndErrorReporting": "Пријављивање дања и грешке", + "reportBug": "Пријaви грешку", + "emailUsMessage": "Пошаљите нам имејл на {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "contactSupport": "Контактирати подршку", + "rateUsOnStore": "Оцените нас на {storeName}", + "blog": "Блог", + "merchandise": "Роба", + "verifyPassword": "Верификујте лозинку", + "pleaseWait": "Молимо сачекајте...", + "generatingEncryptionKeysTitle": "Генерисање кључева за шифровање...", + "recreatePassword": "Поново креирати лозинку", + "recreatePasswordMessage": "Тренутни уређај није довољно моћан да потврди вашу лозинку, тако да је морамо да регенеришемо једном на начин који ради са свим уређајима. \n\nПријавите се помоћу кључа за опоравак и обновите своју лозинку (можете поново користити исту ако желите).", + "useRecoveryKey": "Користите кључ за опоравак", + "incorrectPasswordTitle": "Неисправна лозинка", + "welcomeBack": "Добродошли назад!", + "emailAlreadyRegistered": "Имејл је већ регистрован.", + "emailNotRegistered": "Имејл није регистрован.", + "madeWithLoveAtPrefix": "урађено са ❤️ на ", + "supportDevs": "Претплатити се на ente да би нас подржали", + "supportDiscount": "Употребите купон \"AUTH\" да би добили попуст од 10% прве године", + "changeEmail": "Промени имејл", + "changePassword": "Промени лозинку", + "data": "Подаци", + "importCodes": "Увоз кôдова", + "importTypePlainText": "Обичан текст", + "importTypeEnteEncrypted": "Ente шифрован извоз", + "passwordForDecryptingExport": "Лозинка за дешифровање извоза", + "passwordEmptyError": "Лозинка не може да буде празна", + "importFromApp": "Увоз кôдова од {appName}", + "importGoogleAuthGuide": "Извезите своје рачуне од Google Authenticator на QR кôд помоћу опције \"Трансфер налоге\". Затим помоћу другог уређаја скенирајте QR кôд.\n\nСавет: можете користити веб камеру вашег лаптопа да бисте снимили слику QR кôда.", + "importSelectJsonFile": "Одабрати JSON датотеку", + "importSelectAppExport": "Одабрати извозну датотеку {appName}-а", + "importEnteEncGuide": "Одабрати шифровану извозну JSON датотеку од Ente", + "importRaivoGuide": "Употребите \"Export OTPs to Zip archive\" опцију из подешавања Raivo-а.\n\nИздвојите zip датотеку и увезите JSON датотеку.", + "importBitwardenGuide": "Употребите \"Извоз Сефа\" из Bitwarden и увезите нешифровану JSON датотеку.", + "importAegisGuide": "Употребити \"Export the vault\" из Aegis-а.\n\nАко је сеф шифрован, мораћете унети лозинку сефа да би га дешифровали.", + "import2FasGuide": "Употребити \"Settings->Backup -Export\" из 2FAS-а.\n\nАко је ваша копија шифрирана, мораћете да унесете лозинку за дешифрирање копије", + "importLastpassGuide": "Употребити \"Transfer accounts\" из Lastpass Authenticator и стисните \"Export accounts to file\". Унесите преузет JSON.", + "exportCodes": "Извоз кôдова", + "importLabel": "Увоз", + "importInstruction": "Изаберите датотеку која садржи списак ваших кôдова у следећем формату", + "importCodeDelimiterInfo": "Кôдови се могу одвојити зарезом или новом линијом", + "selectFile": "Изаберите датотеку", + "emailVerificationToggle": "Имејл провера", + "emailVerificationEnableWarning": "Да бисте избегли да се закључате са свог рачуна, обавезно чувајте копију 2ФА имејла ван Ente Auth пре него што омогућите имејл верификацију.", + "authToChangeEmailVerificationSetting": "Потврдите аутентичност да промените верификацији имејл", + "authenticateGeneric": "Молимо потврдите аутентичност", + "authToViewYourRecoveryKey": "Аутентификујте се да бисте погледали кључ за опоравак", + "authToChangeYourEmail": "Аутентификујте се да бисте променили имејл", + "authToChangeYourPassword": "Аутентификујте се да бисте променили лозинку", + "authToViewSecrets": "Аутентификујте се да бисте прегледали Ваше тајне", + "authToInitiateSignIn": "Аутентификујте се да бисте почели пријављивање за копију.", + "ok": "У реду", + "cancel": "Откажи", + "yes": "Да", + "no": "Не", + "email": "Имејл", + "support": "Подршка", + "general": "Опште", + "settings": "Подешавања", + "copied": "Копирано", + "pleaseTryAgain": "Пробајте поново", + "existingUser": "Постојећи корисник", + "newUser": "Нов у Ente", + "delete": "Обриши", + "enterYourPasswordHint": "Унесите лозинку", + "forgotPassword": "Заборавио сам лозинку", + "oops": "Упс", + "suggestFeatures": "Предложи карактеристике", + "faq": "Питања", + "somethingWentWrongMessage": "Нешто је пошло наопако, покушајте поново", + "leaveFamily": "Напусти family претплату", + "leaveFamilyMessage": "Јесте ли сигурни да желите да напустите family чланство?", + "inFamilyPlanMessage": "Имате family чланство!", + "hintForMobile": "Дуго притисните кôд за уређивање или уклањање.", + "hintForDesktop": "Десни клик на кôд за уређивање или уклањање.", + "scan": "Скенирај", + "scanACode": "Скенирај кôд", + "verify": "Верификуј", + "verifyEmail": "Потврди имејл", + "enterCodeHint": "Унесите 6-цифрени кôд из\nапликације за аутентификацију", + "lostDeviceTitle": "Узгубили сте уређај?", + "twoFactorAuthTitle": "Дво-факторска аутентификација", + "passkeyAuthTitle": "Верификација сигурносном кључем", + "verifyPasskey": "Проверите сигурносни кључ", + "loginWithTOTP": "Пријава са TOTP", + "recoverAccount": "Опоравак налога", + "enterRecoveryKeyHint": "Унети кључ за опоравак", + "recover": "Опорави", + "contactSupportViaEmailMessage": "Послати имејл на {email} са регистрованог имејла", + "@contactSupportViaEmailMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "invalidQRCode": "Неважећи QR кôд", + "noRecoveryKeyTitle": "Немате кључ за опоравак?", + "enterEmailHint": "Унесите Ваш имејл", + "enterNewEmailHint": "Унесите Ваш нови имејл", + "invalidEmailTitle": "Погрешна имејл адреса", + "invalidEmailMessage": "Унесите важећи имејл.", + "deleteAccount": "Избриши налог", + "deleteAccountQuery": "Жао нам је што одлазите. Да ли се суочавате са неком грешком?", + "yesSendFeedbackAction": "Да, послати повратне информације", + "noDeleteAccountAction": "Не, избрисати налог", + "initiateAccountDeleteTitle": "Молимо вас да се аутентификујете за брисање рачуна", + "sendEmail": "Шаљи имејл", + "createNewAccount": "Креирај нови налог", + "weakStrength": "Слабо", + "strongStrength": "Јако", + "moderateStrength": "Умерено", + "confirmPassword": "Потврдите лозинку", + "close": "Затвори", + "oopsSomethingWentWrong": "Нешто није у реду.", + "selectLanguage": "Изабери језик", + "language": "Језик", + "social": "Друштвене мреже", + "security": "Безбедност", + "lockscreen": "Закључавање екрана", + "authToChangeLockscreenSetting": "Аутентификујте се да бисте променили закључавање екрана", + "deviceLockEnablePreSteps": "Да бисте омогућили закључавање уређаја, молимо вас да подесите шифру уређаја или закључавање екрана у системским подешавањима.", + "viewActiveSessions": "Видети активне сесије", + "authToViewYourActiveSessions": "Аутентификујте се да бисте преглеадали активне сесије", + "searchHint": "Претрага...", + "search": "Претрага", + "sorryUnableToGenCode": "Извините, не могу да генеришем кôд за {issuerName}", + "noResult": "Нема резултата", + "addCode": "Додај кôд", + "scanAQrCode": "Скенирај QR кôд", + "enterDetailsManually": "Ручно унети детеље", + "edit": "Уреди", + "share": "Подели", + "shareCodes": "Дели кôдове", + "shareCodesDuration": "Изаберите трајање за које желите да поделите кôдове.", + "restore": "Врати", + "copiedToClipboard": "Копирано у оставу", + "copiedNextToClipboard": "Копирали следећи кôд у остави", + "error": "Грешка", + "recoveryKeyCopiedToClipboard": "Кључ за опоравак копирано у остави", + "recoveryKeyOnForgotPassword": "Ако заборавите лозинку, једини начин на који можете повратити податке је са овим кључем.", + "recoveryKeySaveDescription": "Не чувамо овај кључ, молимо да сачувате кључ од 24 речи на сигурном месту.", + "doThisLater": "Уради то касније", + "saveKey": "Сачувај кључ", + "save": "Сачувај", + "send": "Пошаљи", + "saveOrSendDescription": "Да ли желите да ово сачувате у складиште (фасцикли за преузимање подразумевано) или да га пошаљете другим апликацијама?", + "saveOnlyDescription": "Да ли желите да ово сачувате у складиште (фасцикли за преузимање подразумевано)?", + "back": "Назад", + "createAccount": "Направи налог", + "passwordStrength": "Снага лозинке: {passwordStrengthValue}", + "@passwordStrength": { + "description": "Text to indicate the password strength", + "placeholders": { + "passwordStrengthValue": { + "description": "The strength of the password as a string", + "type": "String", + "example": "Weak or Moderate or Strong" + } + }, + "message": "Password Strength: {passwordStrengthText}" + }, + "password": "Лозинка", + "signUpTerms": "Прихватам услове сервиса и политику приватности", + "privacyPolicyTitle": "Политика приватности", + "termsOfServicesTitle": "Услови", + "encryption": "Шифровање", + "setPasswordTitle": "Постави лозинку", + "changePasswordTitle": "Промени лозинку", + "resetPasswordTitle": "Ресетуј лозинку", + "encryptionKeys": "Кључеве шифровања", + "passwordWarning": "Не чувамо ову лозинку, па ако је заборавите, не можемо дешифрирати ваше податке", + "enterPasswordToEncrypt": "Унесите лозинку за употребу за шифровање ваших података", + "enterNewPasswordToEncrypt": "Унесите нову лозинку за употребу за шифровање ваших података", + "passwordChangedSuccessfully": "Лозинка је успешно промењена", + "generatingEncryptionKeys": "Генерисање кључева за шифровање...", + "continueLabel": "Настави", + "insecureDevice": "Уређај није сигуран", + "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Извините, не можемо да генеришемо сигурне кључеве на овом уређају.\n\nМолимо пријавите се са другог уређаја.", + "howItWorks": "Како то функционише", + "ackPasswordLostWarning": "Разумем да ако изгубим лозинку, могу изгубити своје податке пошто су шифрирани од краја до краја.", + "loginTerms": "Кликом на пријаву, прихватам услове сервиса и политику приватности", + "logInLabel": "Пријави се", + "logout": "Одјави ме", + "areYouSureYouWantToLogout": "Да ли сте сигурни да се одјавите?", + "yesLogout": "Да, одјави ме", + "exit": "Излаз", + "theme": "Тема", + "lightTheme": "Светла", + "darkTheme": "Tamna", + "systemTheme": "Систем", + "verifyingRecoveryKey": "Провера кључа за опоравак...", + "recoveryKeyVerified": "Кључ за опоравак је проверен", + "recoveryKeySuccessBody": "Сјајно! Ваш кључ за опоравак важи. Хвала за проверу.\n\nИмајте на уму да задржите кључ за опоравак на сигрном.", + "invalidRecoveryKey": "Кључ за опоравак који сте унели није валидан. Молимо вас да будете сигурни да садржи 24 речи и проверите правопис сваког.\n\nАко сте унели старији кôд за опоравак, проверите да ли је дугачак 64 знака и проверите сваки од њих.", + "recreatePasswordTitle": "Поново креирати лозинку", + "recreatePasswordBody": "Тренутни уређај није довољно моћан да потврди вашу лозинку, али можемо регенерирати на начин који ради са свим уређајима.\n\nПријавите се помоћу кључа за опоравак и обновите своју лозинку (можете поново користити исту ако желите).", + "invalidKey": "Неисправан кључ", + "tryAgain": "Покушај поново", + "viewRecoveryKey": "Видети кључ за опоравак", + "confirmRecoveryKey": "Потврдити кључ за опоравак", + "recoveryKeyVerifyReason": "Ваш кључ за опоравак је једини начин да се врате фотографије ако заборавите лозинку. Можете пронаћи свој кључ за опоравак у Подешавања> Рачун.\n\nОвдје унесите кључ за опоравак да бисте проверили да ли сте га исправно сачували.", + "confirmYourRecoveryKey": "Потврдити кључ за опоравак", + "confirm": "Потврди", + "emailYourLogs": "Имејлирајте извештаје", + "pleaseSendTheLogsTo": "Пошаљите извештаје на \n{toEmail}", + "copyEmailAddress": "Копирати имејл адресу", + "exportLogs": "Извези изештаје", + "enterYourRecoveryKey": "Унети кључ за опоравак", + "tempErrorContactSupportIfPersists": "Изгледа да је нешто погрешно. Покушајте поново након неког времена. Ако грешка настави, обратите се нашем тиму за подршку.", + "networkHostLookUpErr": "Није могуће повезивање са Ente-ом, молимо вас да проверите мрежне поставке и контактирајте подршку ако грешка и даље постоји.", + "networkConnectionRefusedErr": "Није могуће повезивање са Ente-ом, покушајте поново мало касније. Ако грешка настави, обратите се подршци.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Изгледа да је нешто погрешно. Покушајте поново након неког времена. Ако грешка настави, обратите се нашем тиму за подршку.", + "about": "О програму", + "weAreOpenSource": "Користимо отворени извор!", + "privacy": "Приватност", + "terms": "Услови", + "checkForUpdates": "Провери ажурирања", + "checkStatus": "Провери статус", + "downloadUpdate": "Преузми", + "criticalUpdateAvailable": "Критично ажурирање је доступно", + "updateAvailable": "Доступно ажурирање", + "update": "Ажурирај", + "checking": "Провера...", + "youAreOnTheLatestVersion": "Користите најновију верзију", + "warning": "Упозорење", + "exportWarningDesc": "Извозна датотека садржи осетљиве информације. Молимо вас да је чувате на сигурно.", + "iUnderStand": "Разумем", + "@iUnderStand": { + "description": "Text for the button to confirm the user understands the warning" + }, + "authToExportCodes": "Аутентификујте се да бисте извезли кôдове", + "importSuccessTitle": "Jeeee!", + "importSuccessDesc": "Увели сте {count} кôдова!", + "@importSuccessDesc": { + "placeholders": { + "count": { + "description": "The number of codes imported", + "type": "int", + "example": "1" + } + } + }, + "sorry": "Жао ми је", + "importFailureDesc": "Нисам могао да анализирам изабрану датотеку.\nПишите на support@ente.io ако вам је потребна помоћ!", + "pendingSyncs": "Упозорење", + "pendingSyncsWarningBody": "Неки од ваших кôдова нису сачувани.\n\nМолимо вас осигурајте да имате резервну копију за ове кôдове пре него што се одјавите.", + "checkInboxAndSpamFolder": "Молимо вас да проверите примљену пошту (и нежељену пошту) да бисте довршили верификацију", + "tapToEnterCode": "Пипните да бисте унели кôд", + "resendEmail": "Поново послати имејл", + "weHaveSendEmailTo": "Послали смо имејл на {email}", + "@weHaveSendEmailTo": { + "description": "Text to indicate that we have sent a mail to the user", + "placeholders": { + "email": { + "description": "The email address of the user", + "type": "String", + "example": "example@ente.io" + } + } + }, + "manualSort": "Прилагођено", + "editOrder": "Уреди поредак", + "mostFrequentlyUsed": "Често коришћено", + "mostRecentlyUsed": "Недавно коришћено", + "activeSessions": "Активне сесије", + "somethingWentWrongPleaseTryAgain": "Нешто је пошло наопако. Покушајте поново", + "thisWillLogYouOutOfThisDevice": "Ово ће вас одјавити из овог уређаја!", + "thisWillLogYouOutOfTheFollowingDevice": "Ово ће вас одјавити из овог уређаја:", + "terminateSession": "Прекинути сесију?", + "terminate": "Прекини", + "thisDevice": "Овај уређај", + "toResetVerifyEmail": "Да бисте ресетовали лозинку, прво потврдите свој имејл.", + "thisEmailIsAlreadyInUse": "Овај имејл је већ у употреби", + "verificationFailedPleaseTryAgain": "Неуспешна верификација, покушајте поново", + "yourVerificationCodeHasExpired": "Ваш верификациони кôд је истекао", + "incorrectCode": "Погрешан кôд", + "sorryTheCodeYouveEnteredIsIncorrect": "Унет кôд није добар", + "emailChangedTo": "Имејл промењен на {newEmail}", + "authenticationFailedPleaseTryAgain": "Аутентификација није успела, покушајте поново", + "authenticationSuccessful": "Успешна аутентификација!", + "twofactorAuthenticationSuccessfullyReset": "Двофакторска аутентификација успешно рисетирана", + "incorrectRecoveryKey": "Нетачан кључ за опоравак", + "theRecoveryKeyYouEnteredIsIncorrect": "Унети кључ за опоравак је натачан", + "enterPassword": "Унеси лозинку", + "selectExportFormat": "Изабрати формат извоза", + "exportDialogDesc": "Шифровани извоз ће бити заштићен лозинком по вашем избору.", + "encrypted": "Шифровано", + "plainText": "Обичан текст", + "passwordToEncryptExport": "Лозинка за шифровање извоза", + "export": "Извези", + "useOffline": "Користите без резервних копија", + "signInToBackup": "Пријавите се да бисте сачували кôдове", + "singIn": "Пријавите се", + "sigInBackupReminder": "Извезите кôдове да бисте имали резервну копију од које можете да их вратите.", + "offlineModeWarning": "Одлучили сте да наставите без резервних копија. Молимо примите ручне резервне копије да бисте били сигурни да су ваше кодове на сигурном.", + "showLargeIcons": "Прикажи велике иконе", + "compactMode": "Компактни режим", + "shouldHideCode": "Сакриј кодове", + "doubleTapToViewHiddenCode": "Можете да двапут додирнете унос да бисте видели кôд", + "focusOnSearchBar": "Фокус на претрагу на покретање", + "confirmUpdatingkey": "Јесте ли сигурни да желите да ажурирате тајну кључ?", + "minimizeAppOnCopy": "Умањи апликацију после копије", + "editCodeAuthMessage": "Аутентификуј се за уред кôда", + "deleteCodeAuthMessage": "Аутентификуј се за брсање кôда", + "showQRAuthMessage": "Аутентификуј се за приказ QR кôда", + "confirmAccountDeleteTitle": "Потврда брисања рачуна", + "confirmAccountDeleteMessage": "Овај налог је повезан са другим Ente апликацијама, ако користите било коју.\n\nВаши преношени подаци, на свим Ente апликацијама биће заказани за брисање, и ваш рачун ће се трајно избрисати.", + "androidBiometricHint": "Потврдите идентитет", + "@androidBiometricHint": { + "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricNotRecognized": "Нисмо препознали. Покушати поново.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricSuccess": "Успех", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, + "androidCancelButton": "Откажи", + "@androidCancelButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." + }, + "androidSignInTitle": "Потребна аутентификација", + "@androidSignInTitle": { + "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricRequiredTitle": "Потребна је биометрија", + "@androidBiometricRequiredTitle": { + "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters." + }, + "androidDeviceCredentialsRequiredTitle": "Потребни су акредитиви уређаја", + "@androidDeviceCredentialsRequiredTitle": { + "description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters." + }, + "androidDeviceCredentialsSetupDescription": "Потребни су акредитиви уређаја", + "@androidDeviceCredentialsSetupDescription": { + "description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side." + }, + "goToSettings": "Иди на поставке", + "@goToSettings": { + "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." + }, + "androidGoToSettingsDescription": "Биометријска аутентификација није постављена на вашем уређају. Идите на \"Подешавања> Сигурност\" да бисте додали биометријску аутентификацију.", + "@androidGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." + }, + "iOSLockOut": "Биометријска аутентификација је онемогућена. Закључајте и откључите екран да бисте је омогућили.", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + }, + "iOSGoToSettingsDescription": "Биометријска аутентификација није постављена на вашем уређају. Молимо или омогућите Touch ID или Face ID.", + "@iOSGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." + }, + "iOSOkButton": "У реду", + "@iOSOkButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." + }, + "noInternetConnection": "Нема интернет везе", + "pleaseCheckYourInternetConnectionAndTryAgain": "Провери своју везу са интернетом и покушај поново.", + "signOutFromOtherDevices": "Одјави се из других уређаја", + "signOutOtherBody": "Ако мислиш да неко може знати твоју лозинку, можеш приморати одјављивање све остале уређаје које користе твој налог.", + "signOutOtherDevices": "Одјави друге уређаје", + "doNotSignOut": "Не одјави", + "hearUsWhereTitle": "Како сте чули о Ente? (опционо)", + "hearUsExplanation": "Не пратимо инсталацију апликације. Помогло би да нам кажеш како си нас нашао!", + "recoveryKeySaved": "Кључ за опоравак сачуван у фасцикли за преузимање!", + "waitingForBrowserRequest": "Чека се захтев за претраживач...", + "waitingForVerification": "Чека се верификација...", + "passkey": "Кључ за приступ", + "passKeyPendingVerification": "Верификација је још у току", + "loginSessionExpired": "Сесија је истекла", + "loginSessionExpiredDetails": "Ваша сесија је истекла. Молимо пријавите се поново.", + "developerSettingsWarning": "Сигурно желиш да промениш подешавања за програмере?", + "developerSettings": "Подешавања за програмере", + "serverEndpoint": "Крајња тачка сервера", + "invalidEndpoint": "Погрешна крајња тачка", + "invalidEndpointMessage": "Извини, крајња тачка коју си унео је неважећа. Унеси важећу крајњу тачку и покушај поново.", + "endpointUpdatedMessage": "Крајна тачка успешно ажурирана", + "customEndpoint": "Везано за {endpoint}", + "pinText": "Закачи", + "unpinText": "Откачи", + "pinnedCodeMessage": "{code} је прикачен", + "unpinnedCodeMessage": "{code} је одкачен", + "pinned": "Прикачено", + "tags": "Ознаке", + "createNewTag": "Креирај нову ознаку", + "tag": "Ознака", + "create": "Направи", + "editTag": "Уреди ознаку", + "deleteTagTitle": "Обрисати ознаку?", + "deleteTagMessage": "Сигурно желите да избришете ову ознаку? Ова акција је неповратна.", + "somethingWentWrongParsingCode": "Нисмо били у стању да рашчланимо {x} кôдова.", + "updateNotAvailable": "Ажурирање није доступно", + "viewRawCodes": "Погледајте сирове кôдове", + "rawCodes": "Сирове кôдове", + "rawCodeData": "Податак сировог кôда", + "appLock": "Закључавање апликације", + "noSystemLockFound": "Није пронађено ниједно закључавање система", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Да бисте омогућили закључавање апликације, молимо вас да подесите шифру уређаја или закључавање екрана у системским подешавањима.", + "autoLock": "Ауто-закључавање", + "immediately": "Одмах", + "reEnterPassword": "Поново унеси лозинку", + "reEnterPin": "Поново унеси ПИН", + "next": "Следеће", + "tooManyIncorrectAttempts": "Превише погрешних покушаја", + "tapToUnlock": "Додирните да бисте откључали", + "setNewPassword": "Постави нову лозинку", + "deviceLock": "Закључавање уређаја", + "hideContent": "Сакриј садржај", + "hideContentDescriptionAndroid": "Сакрива садржај апликације у пребацивање апликација и онемогућује снимке екрана", + "hideContentDescriptioniOS": "Сакрива садржај апликације у пребацивање апликација", + "autoLockFeatureDescription": "Време након којег се апликација блокира након што је постављенеа у позадину", + "appLockDescription": "Изаберите између заданог закључавање екрана вашег уређаја и прилагођени екран за закључавање са ПИН-ом или лозинком.", + "pinLock": "ПИН клокирање", + "enterPin": "Унеси ПИН", + "setNewPin": "Постави нови ПИН", + "importFailureDescNew": "Није могао да анализира изабрану датотеку.", + "appLockNotEnabled": "Блокирање апликације није упаљено", + "appLockNotEnabledDescription": "Молимо упалие блокирање апликације на Безбедност > Блокирај апликацију", + "authToViewPasskey": "Аутентификујте се да бисте прегледали кључ", + "appLockOfflineModeWarning": "Одлучили сте да наставите без резервних копија. Ако заборавите лозинку, нећете моћи да приступите својим подацима.", + "duplicateCodes": "Дупликатни кодови", + "noDuplicates": "✨ Нема дупликата", + "youveNoDuplicateCodesThatCanBeCleared": "Немате дупликатне кодове који се могу очистити", + "deduplicateCodes": "Дедуплицирај кодове", + "deselectAll": "Поништи избор свега", + "selectAll": "Изабери све", + "deleteDuplicates": "Обриши дупликате", + "plainHTML": "HTML", + "tellUsWhatYouThink": "Реци нам шта мислиш", + "dropReviewiOS": "Напиши мишљење на App Store", + "dropReviewAndroid": "Напиши мишљење на Play Store", + "supportEnte": "Подржи ente", + "giveUsAStarOnGithub": "Дај нам звездицу на Github", + "free5GB": "5GB бесплатно на ente Photos", + "loginWithAuthAccount": "Пријави се са твојим Auth налогом", + "freeStorageOffer": "Попуст од 10% на ente photos", + "freeStorageOfferDescription": "Употребите кôд \"AUTH\" да би добили попуст од 10% прве године", + "advanced": "Напредно", + "algorithm": "Алгоритам", + "type": "Тип", + "period": "Период", + "digits": "Цифре" +} \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index 58e6032e5c..4092736661 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -173,6 +173,7 @@ "invalidQRCode": "Ogiltig QR-kod", "noRecoveryKeyTitle": "Ingen återställningsnyckel?", "enterEmailHint": "Ange din e-postadress", + "enterNewEmailHint": "Ange din nya e-postadress", "invalidEmailTitle": "Ogiltig e-postadress", "invalidEmailMessage": "Ange en giltig e-postadress.", "deleteAccount": "Radera konto", @@ -368,16 +369,19 @@ "signInToBackup": "Logga in för att säkerhetskopiera dina koder", "singIn": "Logga in", "sigInBackupReminder": "Vänligen exportera dina koder för att säkerställa att du har en säkerhetskopia som du kan återställa från.", + "offlineModeWarning": "Du har valt att fortsätta utan säkerhetskopior. Vänligen ta manuella säkerhetskopior för att se till att dina koder är säkra.", "showLargeIcons": "Visa stora ikoner", "compactMode": "Kompakt läge", "shouldHideCode": "Dölj koder", "doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden", "focusOnSearchBar": "Fokusera på sök vid appstart", + "confirmUpdatingkey": "Är du säker på att du vill uppdatera den hemliga nyckeln?", "minimizeAppOnCopy": "Minimera appen vid kopiering", "editCodeAuthMessage": "Autentisera för att redigera kod", "deleteCodeAuthMessage": "Autentisera för att radera kod", "showQRAuthMessage": "Autentisera för att visa QR-kod", "confirmAccountDeleteTitle": "Bekräfta radering av kontot", + "confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente apps, om du använder någon.\n\nDina uppladdade data, över alla Ente appar, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.", "androidBiometricHint": "Verifiera identitet", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -414,6 +418,18 @@ "@goToSettings": { "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." }, + "androidGoToSettingsDescription": "Biometrisk autentisering är inte konfigurerad på din enhet. Gå till \"Inställningar > Säkerhet\" för att lägga till biometrisk autentisering.", + "@androidGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." + }, + "iOSLockOut": "Biometrisk autentisering är inaktiverat. Lås och lås upp din skärm för att aktivera den.", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + }, + "iOSGoToSettingsDescription": "Biometrisk autentisering är inte konfigurerad på din enhet. Aktivera antingen Touch ID eller Face ID på din telefon.", + "@iOSGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." + }, "iOSOkButton": "OK", "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." @@ -421,6 +437,7 @@ "noInternetConnection": "Ingen internetanslutning", "pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen.", "signOutFromOtherDevices": "Logga ut från andra enheter", + "signOutOtherBody": "Om du tror att någon kanske känner till ditt lösenord kan du tvinga alla andra enheter med ditt konto att logga ut.", "signOutOtherDevices": "Logga ut andra enheter", "doNotSignOut": "Logga inte ut", "hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)", @@ -450,6 +467,7 @@ "create": "Skapa", "editTag": "Redigera tagg", "deleteTagTitle": "Radera tagg?", + "deleteTagMessage": "Vill du ta bort den här koden? Det går inte att ångra den här åtgärden.", "somethingWentWrongParsingCode": "Vi kunde inte tolka {x} koder.", "updateNotAvailable": "Uppdateringen är inte tillgänglig", "viewRawCodes": "Visa råa koder", diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index aee7870c06..2500455576 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -6,10 +6,10 @@ "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" }, - "onBoardingBody": "2 Faktörlü Kimlik Doğrulama kodlarınızı koruyun", + "onBoardingBody": "2FA kodlarınızı güvenli bir şekilde yedekleyin", "onBoardingGetStarted": "Başlayın", "setupFirstAccount": "İlk hesabınızı ekleyin", - "importScanQrCode": "Karekod tara", + "importScanQrCode": "QR kod tara", "qrCode": "QR Kodu", "importEnterSetupKey": "Kurulum anahtarını giriniz", "importAccountPageTitle": "Hesap bilgilerinizi girin", @@ -51,11 +51,11 @@ "trashCode": "Kod çöpe atılsın mı?", "trashCodeMessage": "{account} için kodu çöpe atmak istediğinize emin misiniz?", "trash": "Çöp", - "viewLogsAction": "Günlüğü görüntüle", - "sendLogsDescription": "Günlüğünüz hatanızı çözmemize yardımcı olacaktır. Hassas bilginin kaydedilmediğine dikkat etsek de bu günlükleri paylaşmadan önce kontrol etmenizi isteriz.", - "preparingLogsTitle": "Günlük hazırlanıyor...", - "emailLogsTitle": "Günlüğü e-posta olarak gönder", - "emailLogsMessage": "Lütfen günlüğünüzü {email} adresine gönderin", + "viewLogsAction": "Kayıtları görüntüle", + "sendLogsDescription": "Kayıtlarınız hatanızı çözmemize yardımcı olacaktır. Hassas bilginin kaydedilmediğine dikkat etsek de bu günlükleri paylaşmadan önce kontrol etmenizi isteriz.", + "preparingLogsTitle": "Kayıtlar hazırlanıyor...", + "emailLogsTitle": "Kayıtları e-posta olarak gönder", + "emailLogsMessage": "Lütfen kayıtlarınızı {email} adresine gönderin", "@emailLogsMessage": { "placeholders": { "email": { @@ -64,7 +64,7 @@ } }, "copyEmailAction": "E-postayı Kopyala", - "exportLogsAction": "Günlüğü dışa aktar", + "exportLogsAction": "Kayıtları dışa aktar", "reportABug": "Hata bildirin", "crashAndErrorReporting": "Çökme ve hata bildirimi", "reportBug": "Hata bildir", @@ -90,7 +90,7 @@ "welcomeBack": "Tekrar hoş geldiniz!", "emailAlreadyRegistered": "E-posta zaten kayıtlı.", "emailNotRegistered": "E-posta kayıtlı değil.", - "madeWithLoveAtPrefix": "❤️ ile şurada yapılmıştır ", + "madeWithLoveAtPrefix": "❤️ ile yapılmıştır ", "supportDevs": "Bu projeyi desteklemek için ente kanalına abone olun", "supportDiscount": "İlk yılda %10 indirim için \"AUTH\" kupon kodunu kullanın", "changeEmail": "E-posta adresini değiştir", @@ -173,10 +173,11 @@ "invalidQRCode": "Geçersiz QR kodu", "noRecoveryKeyTitle": "Kurtarma anahtarınız yok mu?", "enterEmailHint": "E-posta adresinizi girin", + "enterNewEmailHint": "Yeni e-posta adresinizi girin", "invalidEmailTitle": "Geçersiz e-posta adresi", "invalidEmailMessage": "Lütfen geçerli bir e-posta adresi girin.", "deleteAccount": "Hesabı sil", - "deleteAccountQuery": "Sizin gitmenizi görmekten üzüleceğiz. Bazı problemlerle mi karşılaşıyorsunuz?", + "deleteAccountQuery": "Sizin gittiğinizi görmekten üzüleceğiz. Bazı problemlerle mi karşılaşıyorsunuz?", "yesSendFeedbackAction": "Evet, geri bildirimi gönder", "noDeleteAccountAction": "Hayır, hesabı sil", "initiateAccountDeleteTitle": "Hesap silme işlemini yapabilmek için lütfen kimliğinizi doğrulayın", @@ -253,8 +254,8 @@ "insecureDevice": "Güvenli olmayan cihaz", "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Üzgünüz, bu cihazda güvenli anahtarlar oluşturamadık.\n\nlütfen farklı bir cihazdan kaydolun.", "howItWorks": "Nasıl çalışır", - "ackPasswordLostWarning": "Eğer şifremi kaybedersem, verilerim uçtan uca şifrelendiğinden verilerimi kaybedebileceğimi anladım.", - "loginTerms": "Giriş yaparak, kullanım şartlarını ve gizlilik politikasını onaylıyorum", + "ackPasswordLostWarning": "Eğer şifremi kaybedersem, verilerim uçtan uca şifrelendiğinden verilerimi kaybedebileceğimi anladım.", + "loginTerms": "Giriş yaparak, kullanım şartları nı ve gizlilik politikası nı onaylıyorum", "logInLabel": "Giriş yapın", "logout": "Çıkış yap", "areYouSureYouWantToLogout": "Çıkış yapmak istediğinize emin misiniz?", @@ -277,10 +278,10 @@ "recoveryKeyVerifyReason": "Kurtarma anahtarınız, şifrenizi unutmanız durumunda fotoğraflarınızı kurtarmanın tek yoludur. Kurtarma anahtarınızı Ayarlar > Hesap bölümünde bulabilirsiniz.\n\nDoğru kaydettiğinizi doğrulamak için lütfen kurtarma anahtarınızı buraya girin.", "confirmYourRecoveryKey": "Kurtarma anahtarınızı doğrulayın", "confirm": "Doğrula", - "emailYourLogs": "Günlüklerinizi e-postayla gönderin", - "pleaseSendTheLogsTo": "Lütfen günlükleri şu adrese gönderin\n{toEmail}", + "emailYourLogs": "Kayıtlarınızı e-postayla gönderin", + "pleaseSendTheLogsTo": "Lütfen kayıtları şu adrese gönderin\n{toEmail}", "copyEmailAddress": "E-posta adresini kopyala", - "exportLogs": "Günlüğü dışa aktar", + "exportLogs": "Kayıtları dışa aktar", "enterYourRecoveryKey": "Kurtarma anahtarınızı girin", "tempErrorContactSupportIfPersists": "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin.", "networkHostLookUpErr": "Ente'ye bağlanılamıyor, lütfen ağ ayarlarınızı kontrol edin ve hata devam ederse desteğe başvurun.", @@ -499,10 +500,24 @@ "appLockOfflineModeWarning": "Yedekleme olmadan devam etmeyi seçtiniz. Eğer uygulama parolanızı unutursanız, verilerinize erişiminiz engellenir.", "duplicateCodes": "Yinelenen kodlar", "noDuplicates": "✨ Yinelenen yok", + "youveNoDuplicateCodesThatCanBeCleared": "Temizlenebilecek herhangi bir yinelenen kodunuz yok", "deduplicateCodes": "Kodları tekilleştir", "deselectAll": "Tümünün seçimini kaldır", "selectAll": "Tümünü seç", "deleteDuplicates": "Yinelenenleri sil", "plainHTML": "Sade HTML", - "supportEnte": "Ente'yi destekle" + "tellUsWhatYouThink": "Bize ne düşündüğünü söyle", + "dropReviewiOS": "App Store'da bir inceleme bırakın", + "dropReviewAndroid": "Play Store'da bir inceleme bırakın", + "supportEnte": "Ente'yi destekle", + "giveUsAStarOnGithub": "Github'da bize bir yıldız verin", + "free5GB": "ente Fotoğraflarında 5GB ücretsiz", + "loginWithAuthAccount": "Kimlik Doğrulama hesabınızla giriş yapın", + "freeStorageOffer": "ente fotoğraflarında %10 indirim", + "freeStorageOfferDescription": "İlk yılda %10 indirim almak için \"AUTH\" kodunu kullanın", + "advanced": "Gelişmiş", + "algorithm": "Algoritma", + "type": "Tür", + "period": "Zaman Aralığı", + "digits": "Uzunluk" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_zh_CN.arb b/auth/lib/l10n/arb/app_zh_CN.arb index a7c3a15e07..6301ceaf81 100644 --- a/auth/lib/l10n/arb/app_zh_CN.arb +++ b/auth/lib/l10n/arb/app_zh_CN.arb @@ -173,6 +173,7 @@ "invalidQRCode": "二维码无效", "noRecoveryKeyTitle": "没有恢复密钥吗?", "enterEmailHint": "请输入您的电子邮件地址", + "enterNewEmailHint": "请输入您的新电子邮件地址", "invalidEmailTitle": "无效的电子邮件地址", "invalidEmailMessage": "请输入一个有效的电子邮件地址。", "deleteAccount": "删除账户", @@ -513,5 +514,10 @@ "free5GB": "ente Photos 上 5GB 可用空间", "loginWithAuthAccount": "使用您的认证账户登录", "freeStorageOffer": "购买 ente Photos 可享受 10% 优惠", - "freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣" + "freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣", + "advanced": "高级", + "algorithm": "算法", + "type": "类型", + "period": "周期", + "digits": "数字" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_zh_TW.arb b/auth/lib/l10n/arb/app_zh_TW.arb index d02afb50d7..230c4d2d1f 100644 --- a/auth/lib/l10n/arb/app_zh_TW.arb +++ b/auth/lib/l10n/arb/app_zh_TW.arb @@ -173,6 +173,7 @@ "invalidQRCode": "QR 碼無效", "noRecoveryKeyTitle": "沒有復原密鑰嗎?", "enterEmailHint": "請輸入您的電子郵件地址", + "enterNewEmailHint": "輸入你的新電子郵件地址", "invalidEmailTitle": "無效的電子郵件地址", "invalidEmailMessage": "請輸入一個有效的電子郵件地址。", "deleteAccount": "刪除帳戶", @@ -513,5 +514,10 @@ "free5GB": "ente Photos 上 5GB 可用空間", "loginWithAuthAccount": "使用您的認證帳戶登錄", "freeStorageOffer": "購買 ente Photos 可享受 10% 優惠", - "freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣" + "freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣", + "advanced": "進階", + "algorithm": "演算法", + "type": "類型", + "period": "期間", + "digits": "數位" } \ No newline at end of file diff --git a/auth/lib/locale.dart b/auth/lib/locale.dart index 8f0e0659c3..03b9e294d2 100644 --- a/auth/lib/locale.dart +++ b/auth/lib/locale.dart @@ -38,28 +38,28 @@ const List appSupportedLocales = [ ]; Locale? autoDetectedLocale; -Locale localResolutionCallBack(locales, supportedLocales) { - Locale? languageCodeMatch; - final Map languageCodeToLocale = { - for (Locale supportedLocale in appSupportedLocales) - supportedLocale.languageCode: supportedLocale, - }; - - for (Locale locale in locales) { +// This function takes device locales and supported locales as input +// and returns the best matching locale. +// The device locales are sorted by priority, so the first one is the most preferred. +Locale localResolutionCallBack(onDeviceLocales, supportedLocales) { + final Set languageSupport = {}; + for (Locale supportedLocale in appSupportedLocales) { + languageSupport.add(supportedLocale.languageCode); + } + for (Locale locale in onDeviceLocales) { + // check if exact local is supported, if yes, return it if (appSupportedLocales.contains(locale)) { autoDetectedLocale = locale; return locale; } - - if (languageCodeMatch == null && - languageCodeToLocale.containsKey(locale.languageCode)) { - languageCodeMatch = languageCodeToLocale[locale.languageCode]; - autoDetectedLocale = languageCodeMatch; + // check if language code is supported, if yes, return it + if (languageSupport.contains(locale.languageCode)) { + autoDetectedLocale = locale; + return locale; } } - // Return the first language code match or default to 'en' - return languageCodeMatch ?? const Locale('en'); + return autoDetectedLocale ?? const Locale('en'); } Future getLocale({ diff --git a/auth/lib/main.dart b/auth/lib/main.dart index c747ac41eb..b0adde52fa 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -87,19 +87,6 @@ void main() async { } } -// Future whiteListLetsEncryptRootCA() async { -// try { -// // https://stackoverflow.com/a/71090239 -// // https://github.com/ente-io/ente/issues/2178 -// ByteData data = -// await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); -// SecurityContext.defaultContext -// .setTrustedCertificatesBytes(data.buffer.asUint8List()); -// } catch (e) { -// _logger.severe("Failed to whitelist Let's Encrypt Root CA", e); -// } -// } - Future _runInForeground() async { final savedThemeMode = _themeMode(await AdaptiveTheme.getThemeMode()); return await _runWithLogs(() async { @@ -116,7 +103,7 @@ Future _runInForeground() async { AppLock( builder: (args) => App(locale: locale), lockScreen: const LockScreen(), - enabled: await Configuration.instance.shouldShowLockScreen(), + enabled: await LockScreenSettings.instance.shouldShowLockScreen(), locale: locale, lightTheme: lightThemeData, darkTheme: darkThemeData, diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart index a04e3e5be4..0bf16c7e62 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -293,10 +293,6 @@ class _SetupEnterSecretKeyPageState extends State { ], ), const SizedBox(height: 12), - widget.code == null - ? advanceOptionWidget() - : const SizedBox.shrink(), - const SizedBox(height: 12), Wrap( spacing: 12, alignment: WrapAlignment.start, @@ -343,6 +339,10 @@ class _SetupEnterSecretKeyPageState extends State { ), ], ), + const SizedBox(height: 12), + widget.code == null + ? advanceOptionWidget() + : const SizedBox.shrink(), const SizedBox(height: 40), SizedBox( width: 400, @@ -525,6 +525,7 @@ class _SetupEnterSecretKeyPageState extends State { } Widget advanceOptionWidget() { + final l10n = context.l10n; return Padding( padding: const EdgeInsets.only(top: 16.0), child: Column( @@ -537,9 +538,7 @@ class _SetupEnterSecretKeyPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Advanced', - ), + Text(l10n.advanced), ValueListenableBuilder( valueListenable: showAdvancedOptions, builder: (context, isExpanded, child) { @@ -583,7 +582,7 @@ class _SetupEnterSecretKeyPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - const FieldLabel("Algorithm", width: 60), + FieldLabel(l10n.algorithm, width: 60), AlgorithmSelectorWidget( currentAlgorithm: _algorithm, onSelected: (newAlgorithm) async { @@ -597,7 +596,7 @@ class _SetupEnterSecretKeyPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - const FieldLabel("Type", width: 60), + FieldLabel(l10n.type, width: 60), ToptSelectorWidget( currentTopt: _type, onSelected: (newTopt) async { @@ -610,7 +609,7 @@ class _SetupEnterSecretKeyPageState extends State { ), Row( children: [ - const FieldLabel("Period", width: 60), + FieldLabel(l10n.period, width: 60), Expanded( child: TextFormField( keyboardType: TextInputType.number, @@ -639,7 +638,7 @@ class _SetupEnterSecretKeyPageState extends State { ), Row( children: [ - const FieldLabel("Digits", width: 60), + FieldLabel(l10n.digits, width: 60), Expanded( child: TextFormField( keyboardType: TextInputType.number, diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index ee0ad32bd4..559e85f81a 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -13,6 +13,7 @@ import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/auth_key.dart'; import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/store/offline_authenticator_db.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; @@ -194,8 +195,13 @@ class AuthenticatorService { final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0; _logger.info("Current sync is $lastSyncTime"); const int fetchLimit = 500; - final List result = + late final List result; + late final int? epochTimeInMicroseconds; + (result, epochTimeInMicroseconds) = await _gateway.getDiff(lastSyncTime, limit: fetchLimit); + PreferenceService.instance + .computeAndStoreTimeOffset(epochTimeInMicroseconds); + _logger.info("${result.length} entries fetched from remote"); if (result.isEmpty) { return; diff --git a/auth/lib/services/local_authentication_service.dart b/auth/lib/services/local_authentication_service.dart index 02c609591d..1502b2a36e 100644 --- a/auth/lib/services/local_authentication_service.dart +++ b/auth/lib/services/local_authentication_service.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart'; import 'package:ente_auth/ui/settings/lock_screen/lock_screen_pin.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; @@ -42,7 +41,7 @@ class LocalAuthenticationService { isAuthenticatingForInAppChange: true, ); AppLock.of(context)!.setEnabled( - await Configuration.instance.shouldShowLockScreen(), + await LockScreenSettings.instance.shouldShowLockScreen(), ); if (!result) { showToast(context, infoMessage); @@ -114,12 +113,13 @@ class LocalAuthenticationService { ); if (result) { AppLock.of(context)!.setEnabled(shouldEnableLockScreen); - await Configuration.instance + await LockScreenSettings.instance .setSystemLockScreen(shouldEnableLockScreen); return true; } else { - AppLock.of(context)! - .setEnabled(await Configuration.instance.shouldShowLockScreen()); + AppLock.of(context)!.setEnabled( + await LockScreenSettings.instance.shouldShowLockScreen(), + ); } } else { // ignore: unawaited_futures diff --git a/auth/lib/services/preference_service.dart b/auth/lib/services/preference_service.dart index 45a2cf0e66..773ee1b149 100644 --- a/auth/lib/services/preference_service.dart +++ b/auth/lib/services/preference_service.dart @@ -18,6 +18,7 @@ class PreferenceService { late final SharedPreferences _prefs; static const kHasShownCoachMarkKey = "has_shown_coach_mark_v2"; + static const kLocalTimeOffsetKey = "local_time_offset"; static const kShouldShowLargeIconsKey = "should_show_large_icons"; static const kShouldHideCodesKey = "should_hide_codes"; static const kShouldAutoFocusOnSearchBar = "should_auto_focus_on_search_bar"; @@ -114,4 +115,24 @@ class PreferenceService { return installedTimeinMillis; } } + + // localEpochOffsetInMilliSecond returns the local epoch offset in milliseconds. + // This is used to adjust the time for TOTP calculations when device local time is not in sync with actual time. + int timeOffsetInMilliSeconds() { + return _prefs.getInt(kLocalTimeOffsetKey) ?? 0; + } + + void computeAndStoreTimeOffset( + int? epochTimeInMicroseconds, + ) { + if (epochTimeInMicroseconds == null) { + _prefs.remove(kLocalTimeOffsetKey); + return; + } + int serverEpochTimeInMilliSecond = epochTimeInMicroseconds ~/ 1000; + int localEpochTimeInMilliSecond = DateTime.now().millisecondsSinceEpoch; + int localEpochOffset = + serverEpochTimeInMilliSecond - localEpochTimeInMilliSecond; + _prefs.setInt(kLocalTimeOffsetKey, localEpochOffset); + } } diff --git a/auth/lib/ui/account/change_email_dialog.dart b/auth/lib/ui/account/change_email_dialog.dart index 828970cd73..5277e0fcb6 100644 --- a/auth/lib/ui/account/change_email_dialog.dart +++ b/auth/lib/ui/account/change_email_dialog.dart @@ -18,7 +18,7 @@ class _ChangeEmailDialogState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return AlertDialog( - title: Text(l10n.enterEmailHint), + title: Text(l10n.enterNewEmailHint), content: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index 520a18d76c..58386fe438 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -7,10 +7,12 @@ import 'package:flutter/material.dart'; class CodeTimerProgress extends StatefulWidget { final int period; final bool isCompactMode; + final int timeOffsetInMilliseconds; const CodeTimerProgress({ super.key, required this.period, this.isCompactMode = false, + this.timeOffsetInMilliseconds = 0, }); @override @@ -20,7 +22,7 @@ class CodeTimerProgress extends StatefulWidget { class _CodeTimerProgressState extends State { late final Timer _timer; late final ValueNotifier _progress; - late final int _periodInMicros; + late final int _periodInMilii; // Reduce update frequency final int _updateIntervalMs = @@ -29,29 +31,30 @@ class _CodeTimerProgressState extends State { @override void initState() { super.initState(); - _periodInMicros = widget.period * 1000000; + _periodInMilii = widget.period * 1000; _progress = ValueNotifier(0.0); - _updateTimeRemaining(DateTime.now().microsecondsSinceEpoch); + _updateTimeRemaining(DateTime.now().millisecondsSinceEpoch); _timer = Timer.periodic(Duration(milliseconds: _updateIntervalMs), (timer) { - final now = DateTime.now().microsecondsSinceEpoch; + final now = DateTime.now().millisecondsSinceEpoch; _updateTimeRemaining(now); }); } - void _updateTimeRemaining(int currentMicros) { + void _updateTimeRemaining(int currentMilliSeconds) { // More efficient time calculation using modulo - final elapsed = (currentMicros) % _periodInMicros; - final timeRemaining = _periodInMicros - elapsed; - _progress.value = timeRemaining / _periodInMicros; + final elapsed = (currentMilliSeconds + widget.timeOffsetInMilliseconds) % + _periodInMilii; + final timeRemaining = _periodInMilii - elapsed; + _progress.value = timeRemaining / _periodInMilii; } @override void didUpdateWidget(covariant CodeTimerProgress oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.period != widget.period) { - _periodInMicros = widget.period * 1000000; - _updateTimeRemaining(DateTime.now().microsecondsSinceEpoch); + _periodInMilii = widget.period * 1000; + _updateTimeRemaining(DateTime.now().millisecondsSinceEpoch); } } diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 3ccb7ab67b..be9c2b8a69 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -152,6 +152,8 @@ class _CodeWidgetState extends State { key: ValueKey('period_${widget.code.period}'), period: widget.code.period, isCompactMode: widget.isCompactMode, + timeOffsetInMilliseconds: + PreferenceService.instance.timeOffsetInMilliSeconds(), ), widget.isCompactMode ? const SizedBox(height: 4) diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index cd522b7746..3a3d650259 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -319,7 +319,7 @@ class _HomePageState extends State { Future navigateToLockScreen() async { final bool shouldShowLockScreen = - await Configuration.instance.shouldShowLockScreen(); + await LockScreenSettings.instance.shouldShowLockScreen(); if (shouldShowLockScreen) { await AppLock.of(context)!.showLockScreen(); } else { diff --git a/auth/lib/ui/settings/lock_screen/lock_screen_options.dart b/auth/lib/ui/settings/lock_screen/lock_screen_options.dart index 9f53f10248..7f48bca84d 100644 --- a/auth/lib/ui/settings/lock_screen/lock_screen_options.dart +++ b/auth/lib/ui/settings/lock_screen/lock_screen_options.dart @@ -1,7 +1,6 @@ import "dart:async"; import "dart:io"; -import "package:ente_auth/core/configuration.dart"; import "package:ente_auth/l10n/l10n.dart"; import "package:ente_auth/services/local_authentication_service.dart"; import "package:ente_auth/theme/ente_theme.dart"; @@ -31,8 +30,7 @@ class LockScreenOptions extends StatefulWidget { } class _LockScreenOptionsState extends State { - final Configuration _configuration = Configuration.instance; - final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance; + final LockScreenSettings _lockScreenSettings = LockScreenSettings.instance; late bool appLock = false; bool isPinEnabled = false; bool isPasswordEnabled = false; @@ -43,18 +41,18 @@ class _LockScreenOptionsState extends State { @override void initState() { super.initState(); - hideAppContent = _lockscreenSetting.getShouldHideAppContent(); - autoLockTimeInMilliseconds = _lockscreenSetting.getAutoLockTime(); + hideAppContent = _lockScreenSettings.getShouldHideAppContent(); + autoLockTimeInMilliseconds = _lockScreenSettings.getAutoLockTime(); _initializeSettings(); - appLock = _lockscreenSetting.getIsAppLockSet(); + appLock = _lockScreenSettings.getIsAppLockSet(); } Future _initializeSettings() async { - final bool passwordEnabled = await _lockscreenSetting.isPasswordSet(); - final bool pinEnabled = await _lockscreenSetting.isPinSet(); + final bool passwordEnabled = await _lockScreenSettings.isPasswordSet(); + final bool pinEnabled = await _lockScreenSettings.isPinSet(); final bool shouldHideAppContent = - _lockscreenSetting.getShouldHideAppContent(); - final bool systemLockEnabled = _configuration.shouldShowSystemLockScreen(); + _lockScreenSettings.getShouldHideAppContent(); + final bool systemLockEnabled = _lockScreenSettings.shouldShowSystemLockScreen(); setState(() { isPasswordEnabled = passwordEnabled; isPinEnabled = pinEnabled; @@ -66,8 +64,8 @@ class _LockScreenOptionsState extends State { Future _deviceLock() async { if (await LocalAuthenticationService.instance .isLocalAuthSupportedOnDevice()) { - await _lockscreenSetting.removePinAndPassword(); - await _configuration.setSystemLockScreen(!isSystemLockEnabled); + await _lockScreenSettings.removePinAndPassword(); + await _lockScreenSettings.setSystemLockScreen(!isSystemLockEnabled); } else { await showDialogWidget( context: context, @@ -96,10 +94,10 @@ class _LockScreenOptionsState extends State { ); if (result) { - await _configuration.setSystemLockScreen(false); - await _lockscreenSetting.setAppLockEnabled(true); + await _lockScreenSettings.setSystemLockScreen(false); + await _lockScreenSettings.setAppLockEnabled(true); setState(() { - appLock = _lockscreenSetting.getIsAppLockSet(); + appLock = _lockScreenSettings.getIsAppLockSet(); }); } await _initializeSettings(); @@ -114,9 +112,9 @@ class _LockScreenOptionsState extends State { ), ); if (result) { - await _configuration.setSystemLockScreen(false); + await _lockScreenSettings.setSystemLockScreen(false); setState(() { - appLock = _lockscreenSetting.getIsAppLockSet(); + appLock = _lockScreenSettings.getIsAppLockSet(); }); } await _initializeSettings(); @@ -126,17 +124,17 @@ class _LockScreenOptionsState extends State { AppLock.of(context)!.setEnabled(!appLock); if (await LocalAuthenticationService.instance .isLocalAuthSupportedOnDevice()) { - await _configuration.setSystemLockScreen(!appLock); - await _lockscreenSetting.setAppLockEnabled(!appLock); + await _lockScreenSettings.setSystemLockScreen(!appLock); + await _lockScreenSettings.setAppLockEnabled(!appLock); } else { - await _configuration.setSystemLockScreen(false); - await _lockscreenSetting.setAppLockEnabled(false); + await _lockScreenSettings.setSystemLockScreen(false); + await _lockScreenSettings.setAppLockEnabled(false); } - await _lockscreenSetting.removePinAndPassword(); + await _lockScreenSettings.removePinAndPassword(); if (PlatformUtil.isMobile()) { - await _lockscreenSetting.setHideAppContent(!appLock); + await _lockScreenSettings.setHideAppContent(!appLock); setState(() { - hideAppContent = _lockscreenSetting.getShouldHideAppContent(); + hideAppContent = _lockScreenSettings.getShouldHideAppContent(); }); } await _initializeSettings(); @@ -152,7 +150,7 @@ class _LockScreenOptionsState extends State { ).then( (value) { setState(() { - autoLockTimeInMilliseconds = _lockscreenSetting.getAutoLockTime(); + autoLockTimeInMilliseconds = _lockScreenSettings.getAutoLockTime(); }); }, ); @@ -162,7 +160,7 @@ class _LockScreenOptionsState extends State { setState(() { hideAppContent = !hideAppContent; }); - await _lockscreenSetting.setHideAppContent(hideAppContent); + await _lockScreenSettings.setHideAppContent(hideAppContent); } String _formatTime(Duration duration) { diff --git a/auth/lib/ui/settings/security_section_widget.dart b/auth/lib/ui/settings/security_section_widget.dart index 41b04c80b5..dab16cb56f 100644 --- a/auth/lib/ui/settings/security_section_widget.dart +++ b/auth/lib/ui/settings/security_section_widget.dart @@ -166,7 +166,7 @@ class _SecuritySectionWidgetState extends State { return; } } - if (await Configuration.instance.shouldShowLockScreen()) { + if (await LockScreenSettings.instance.shouldShowLockScreen()) { final bool result = await requestAuthentication( context, context.l10n.authToChangeLockscreenSetting, diff --git a/auth/lib/utils/lock_screen_settings.dart b/auth/lib/utils/lock_screen_settings.dart index b87323e37d..247f7ae4b2 100644 --- a/auth/lib/utils/lock_screen_settings.dart +++ b/auth/lib/utils/lock_screen_settings.dart @@ -3,6 +3,8 @@ import "dart:io"; import "dart:typed_data"; import "package:ente_auth/core/configuration.dart"; +import "package:ente_auth/core/event_bus.dart"; +import "package:ente_auth/events/signed_out_event.dart"; import "package:ente_auth/utils/platform_util.dart"; import "package:ente_crypto_dart/ente_crypto_dart.dart"; import "package:flutter/material.dart"; @@ -26,6 +28,7 @@ class LockScreenSettings { static const keyHasMigratedLockScreenChanges = "ls_has_migrated_lock_screen_changes"; static const keyShowOfflineModeWarning = "ls_show_offline_mode_warning"; + static const keyShouldShowLockScreen = "should_show_lock_screen"; static const String kIsLightMode = "is_light_mode"; final List autoLockDurations = const [ @@ -52,6 +55,10 @@ class LockScreenSettings { await runLockScreenChangesMigration(); await _clearLsDataInKeychainIfFreshInstall(); + + Bus.instance.on().listen((event) { + removePinAndPassword(); + }); } Future setOfflineModeWarningStatus(bool value) async { @@ -69,8 +76,7 @@ class LockScreenSettings { final bool passwordEnabled = await isPasswordSet(); final bool pinEnabled = await isPinSet(); - final bool systemLockEnabled = - Configuration.instance.shouldShowSystemLockScreen(); + final bool systemLockEnabled = shouldShowSystemLockScreen(); if (passwordEnabled || pinEnabled || systemLockEnabled) { await setAppLockEnabled(true); @@ -214,6 +220,24 @@ class LockScreenSettings { return await _secureStorage.containsKey(key: password); } + Future shouldShowLockScreen() async { + final bool isPin = await isPinSet(); + final bool isPass = await isPasswordSet(); + return isPin || isPass || shouldShowSystemLockScreen(); + } + + bool shouldShowSystemLockScreen() { + if (_preferences.containsKey(keyShouldShowLockScreen)) { + return _preferences.getBool(keyShouldShowLockScreen)!; + } else { + return false; + } + } + + Future setSystemLockScreen(bool value) { + return _preferences.setBool(keyShouldShowLockScreen, value); + } + // If the app was uninstalled (without logging out if it was used with // backups), keychain items of the app persist in the keychain. To avoid using // old keychain items, we delete them on reinstall. diff --git a/auth/lib/utils/totp_util.dart b/auth/lib/utils/totp_util.dart index 6b7f53ae5b..53b68be5ff 100644 --- a/auth/lib/utils/totp_util.dart +++ b/auth/lib/utils/totp_util.dart @@ -1,8 +1,14 @@ import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/services/preference_service.dart'; import 'package:flutter/foundation.dart'; import 'package:otp/otp.dart' as otp; import 'package:steam_totp/steam_totp.dart'; +int millisecondsSinceEpoch() { + return DateTime.now().millisecondsSinceEpoch + + PreferenceService.instance.timeOffsetInMilliSeconds(); +} + String getOTP(Code code) { if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { return _getSteamCode(code); @@ -12,7 +18,7 @@ String getOTP(Code code) { } return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret), - DateTime.now().millisecondsSinceEpoch, + millisecondsSinceEpoch(), length: code.digits, interval: code.period, algorithm: _getAlgorithm(code), @@ -34,7 +40,7 @@ String _getSteamCode(Code code, [bool isNext = false]) { final SteamTOTP steamtotp = SteamTOTP(secret: code.secret); return steamtotp.generate( - DateTime.now().millisecondsSinceEpoch ~/ 1000 + (isNext ? code.period : 0), + millisecondsSinceEpoch() ~/ 1000 + (isNext ? code.period : 0), ); } @@ -44,7 +50,7 @@ String getNextTotp(Code code) { } return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret), - DateTime.now().millisecondsSinceEpoch + code.period * 1000, + millisecondsSinceEpoch() + code.period * 1000, length: code.digits, interval: code.period, algorithm: _getAlgorithm(code), @@ -56,9 +62,7 @@ String getNextTotp(Code code) { // It returns the start time and a list of future codes. (int, List) generateFutureTotpCodes(Code code, int count) { final int startTime = - ((DateTime.now().millisecondsSinceEpoch ~/ 1000) ~/ code.period) * - code.period * - 1000; + ((millisecondsSinceEpoch() ~/ 1000) ~/ code.period) * code.period * 1000; final String secret = getSanitizedSecret(code.secret); final List codes = []; if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { diff --git a/auth/linux/packaging/enteauth.appdata.xml b/auth/linux/packaging/enteauth.appdata.xml index b6682a60ce..6785961a91 100644 --- a/auth/linux/packaging/enteauth.appdata.xml +++ b/auth/linux/packaging/enteauth.appdata.xml @@ -18,6 +18,8 @@ + + @@ -33,4 +35,4 @@ #ffffff #000000 - \ No newline at end of file + diff --git a/auth/macos/Podfile.lock b/auth/macos/Podfile.lock index 19fe954a53..3506dde0ea 100644 --- a/auth/macos/Podfile.lock +++ b/auth/macos/Podfile.lock @@ -34,11 +34,11 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - Sentry/HybridSDK (8.36.0) - - sentry_flutter (8.9.0): + - Sentry/HybridSDK (8.46.0) + - sentry_flutter (8.14.2): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.36.0) + - Sentry/HybridSDK (= 8.46.0) - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -157,33 +157,33 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db - cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_saver: 44e6fbf666677faf097302460e214e977fdd977b - flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b - flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 - flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 - flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343 + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c + device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 + file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f + flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d + flutter_local_authentication: 2f9a2682f498abcc12d7e9729b5007a947170fdc + flutter_local_notifications: 453432cd6399a07d072885bc7828fb2307868856 + flutter_secure_storage_macos: b2d62a774c23b060f0b99d0173b0b36abb4a8632 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 - objective_c: e5f8194456e8fc943e034d1af00510a1bc29c067 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 - sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe - share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + package_info_plus: a8a591e70e87ce97ce5d21b2594f69cea9e0312f + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936 + Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 + share_plus: 11c7b7fa7020465584eca3ff6392c5bc1e399d6e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sodium_libs: b9459e5bfc1185349f43472e79fc5d8e526b2bda + sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b - tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqlite3_flutter_libs: 03311aede9d32fb2d24e32bebb8cd01c3b2e6239 + tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 6ff827273ace187339fc5d3684072a26ad85c298 diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 39118e7201..fa632892e2 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -675,14 +675,13 @@ packages: source: hosted version: "9.2.2" flutter_secure_storage_linux: - dependency: "direct overridden" + dependency: transitive description: - path: flutter_secure_storage_linux - ref: develop - resolved-ref: "5a5692b609b3886cdd49b2ed06b9c079ecdff996" - url: "https://github.com/mogol/flutter_secure_storage.git" - source: git - version: "1.2.1" + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" flutter_secure_storage_macos: dependency: transitive description: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 4b3a24dbb8..bdb32b695b 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,7 +1,7 @@ name: ente_auth description: ente two-factor authenticator -version: 4.3.5+436 +version: 4.4.0+440 publish_to: none environment: @@ -107,12 +107,6 @@ dependencies: window_manager: ^0.4.2 xdg_directories: ^1.0.4 -dependency_overrides: - flutter_secure_storage_linux: - git: - url: https://github.com/mogol/flutter_secure_storage.git - ref: develop - path: flutter_secure_storage_linux dev_dependencies: build_runner: ^2.1.11 flutter_test: diff --git a/auth/scripts/release_tag.sh b/auth/scripts/release_tag.sh new file mode 100755 index 0000000000..f35c89ab4d --- /dev/null +++ b/auth/scripts/release_tag.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Function to display usage +usage() { + echo "Usage: $0 tag" + exit 1 +} + +# Ensure a tag was provided +[[ $# -eq 0 ]] && usage + +# Exit immediately if a command exits with a non-zero status +set -e + +# Go to the project root directory +cd "$(dirname "$0")/.." + +# Get the tag from the command line argument +TAG=$1 + +# Define the appdata file path - use absolute path to avoid directory navigation issues +PROJECT_ROOT=$(pwd) +APPDATA_FILE="${PROJECT_ROOT}/linux/packaging/enteauth.appdata.xml" + +# Get the version from the pubspec.yaml file and cut everything after the + +VERSION=$(grep "^version:" pubspec.yaml | awk '{ print $2 }' | cut -d '+' -f 1) + +PREFIX="auth-v" + +# Ensure the tag has the correct prefix +if [[ $TAG != $PREFIX* ]]; then + echo "Invalid tag. tags must start with '$PREFIX'." + exit 1 +fi + +# Ensure the tag version is in the pubspec.yaml file +if [[ $TAG != *$VERSION ]]; then + echo "Invalid tag." + echo "The version $VERSION in pubspec doesn't match the version in tag $TAG" + exit 1 +fi + +# Extract version number from the tag (remove prefix) +TAG_VERSION=${TAG#$PREFIX} + +# Check if this version is already in the releases section of the appdata.xml file +if ! grep -q "/{print $0; print " "; next}1' "$APPDATA_FILE" > "${APPDATA_FILE}.tmp" + mv "${APPDATA_FILE}.tmp" "$APPDATA_FILE" + + echo "Added release entry for version $TAG_VERSION with date $TODAY" + + # Stage and commit the updated appdata.xml file + git add "$APPDATA_FILE" + git commit -m "Add release $TAG_VERSION to appdata.xml" + echo "Committed appdata.xml changes for version $TAG_VERSION" +fi + +# If all checks pass, create the tag +git tag $TAG +echo "Tag $TAG created." + +exit 0 \ No newline at end of file diff --git a/cli/internal/api/login.go b/cli/internal/api/login.go index 61491f1130..5706422516 100644 --- a/cli/internal/api/login.go +++ b/cli/internal/api/login.go @@ -83,13 +83,14 @@ func (c *Client) VerifySRPSession( return &res, nil } -func (c *Client) SendEmailOTP( +func (c *Client) SendLoginOTP( ctx context.Context, email string, ) error { var res AuthorizationResponse payload := map[string]interface{}{ - "email": email, + "email": email, + "purpose": "login", } r, err := c.restClient.R(). SetContext(ctx). diff --git a/cli/pkg/sign_in.go b/cli/pkg/sign_in.go index 4218c5b60b..7fb54738db 100644 --- a/cli/pkg/sign_in.go +++ b/cli/pkg/sign_in.go @@ -167,7 +167,7 @@ func (c *ClICtrl) verifyPassKey(ctx context.Context, authResp *api.Authorization } func (c *ClICtrl) validateEmail(ctx context.Context, email string) (*api.AuthorizationResponse, error) { - err := c.Client.SendEmailOTP(ctx, email) + err := c.Client.SendLoginOTP(ctx, email) if err != nil { return nil, err } diff --git a/desktop/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml index fa8c8e53ef..a3b8b38a13 100644 --- a/desktop/.github/workflows/desktop-release.yml +++ b/desktop/.github/workflows/desktop-release.yml @@ -113,6 +113,11 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Windows Azure Trusted Signing related values + # https://www.electron.build/code-signing-win#using-azure-trusted-signing-beta + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # Default is "draft", but since our nightly builds update # existing pre-releases, set this to "prerelease". EP_PRE_RELEASE: true diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index df8ea6b9bd..22d6ad2309 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,9 +1,19 @@ # CHANGELOG -## v1.7.13 (Unreleased) +## v1.7.14 (Unreleased) +- Increase file size limit to 10 GB. - . +## v1.7.13 + +- Generate streams for videos (beta) + + > Streamable videos can be enabled in Preferences. For more details, see the + > [video streaming FAQ](https://help.ente.io/photos/faq/video-streaming). + +- Support Turkish translations. + ## v1.7.12 - Improved video player with streaming support (for already processed videos). diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index 42d152dc4d..0fb94370ae 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -14,6 +14,11 @@ win: target: - target: nsis arch: [x64, arm64] + azureSignOptions: + publisherName: ENTE TECHNOLOGIES, INC. + endpoint: https://eus.codesigning.azure.net/ + certificateProfileName: EnteTrustCertProfile + codeSigningAccountName: EnteTechnologiesInc nsis: deleteAppDataOnUninstall: true linux: diff --git a/desktop/package.json b/desktop/package.json index 63b79becc7..b84cb9cae0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.13-beta", + "version": "1.7.14-beta", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", @@ -33,30 +33,31 @@ "compare-versions": "^6.1.1", "electron-log": "^5.4.0", "electron-store": "^8.2.0", - "electron-updater": "^6.6.3", + "electron-updater": "^6.6.5", "ffmpeg-static": "^5.2.0", "lru-cache": "^11.1.0", "next-electron-server": "^1.0.0", "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.20.1" + "onnxruntime-node": "^1.20.1", + "zod": "^3.25.51" }, "devDependencies": { - "@eslint/js": "^9.25.1", - "@tsconfig/node22": "^22.0.1", + "@eslint/js": "^9.28.0", + "@tsconfig/node22": "^22.0.2", "@types/auto-launch": "^5.0.5", "@types/ffmpeg-static": "^3.0.3", "ajv": "^8.17.1", "concurrently": "^9.1.2", "cross-env": "^7.0.3", - "electron": "^36.1.0", - "electron-builder": "^26.0.14", + "electron": "^36.4.0", + "electron-builder": "^26.0.17", "eslint": "^9", "prettier": "3.5.3", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-packagejson": "^2.5.10", - "shx": "^0.3.4", + "prettier-plugin-packagejson": "^2.5.15", + "shx": "^0.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1" + "typescript-eslint": "^8.33.1" }, "packageManager": "yarn@1.22.22", "productName": "ente" diff --git a/desktop/src/main.ts b/desktop/src/main.ts index dd6cab4a18..687d8b11d6 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -78,6 +78,14 @@ export const allowWindowClose = (): void => { * We call this at the end of this file. */ const main = () => { + // Workaround for Electron 36 not launching on some Linux distros. Remove + // once fixed or otherwise mitigated upstream. + // + // https://github.com/electron/electron/issues/46538#issuecomment-2808806722 + if (process.platform == "linux") { + app.commandLine.appendSwitch("gtk-version", "3"); + } + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index b99fae7173..ce977a5088 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -16,6 +16,7 @@ import type { FFmpegCommand, FolderWatch, PendingUploads, + UtilityProcessType, ZipItem, } from "../types/ipc"; import { logToDisk } from "./log"; @@ -31,7 +32,7 @@ import { openLogDirectory, selectDirectory, } from "./services/dir"; -import { ffmpegExec } from "./services/ffmpeg"; +import { ffmpegDetermineVideoDuration, ffmpegExec } from "./services/ffmpeg"; import { fsExists, fsFindFiles, @@ -47,11 +48,10 @@ import { } from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { logout } from "./services/logout"; -import { createMLWorker } from "./services/ml"; import { lastShownChangelogVersion, - masterKeyB64, - saveMasterKeyB64, + masterKeyFromSafeStorage, + saveMasterKeyInSafeStorage, setLastShownChangelogVersion, } from "./services/store"; import { @@ -70,6 +70,7 @@ import { watchUpdateIgnoredFiles, watchUpdateSyncedFiles, } from "./services/watch"; +import { triggerCreateUtilityProcess } from "./services/workers"; /** * Listen for IPC events sent/invoked by the renderer process, and route them to @@ -107,10 +108,12 @@ export const attachIPCHandlers = () => { ipcMain.handle("selectDirectory", () => selectDirectory()); - ipcMain.handle("masterKeyB64", () => masterKeyB64()); + ipcMain.handle("masterKeyFromSafeStorage", () => + masterKeyFromSafeStorage(), + ); - ipcMain.handle("saveMasterKeyB64", (_, masterKeyB64: string) => - saveMasterKeyB64(masterKeyB64), + ipcMain.handle("saveMasterKeyInSafeStorage", (_, masterKey: string) => + saveMasterKeyInSafeStorage(masterKey), ); ipcMain.handle("lastShownChangelogVersion", () => @@ -181,10 +184,10 @@ export const attachIPCHandlers = () => { "generateImageThumbnail", ( _, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, - ) => generateImageThumbnail(dataOrPathOrZipItem, maxDimension, maxSize), + ) => generateImageThumbnail(pathOrZipItem, maxDimension, maxSize), ); ipcMain.handle( @@ -192,9 +195,15 @@ export const attachIPCHandlers = () => { ( _, command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, - ) => ffmpegExec(command, dataOrPathOrZipItem, outputFileExtension), + ) => ffmpegExec(command, pathOrZipItem, outputFileExtension), + ); + + ipcMain.handle( + "ffmpegDetermineVideoDuration", + (_, pathOrZipItem: string | ZipItem) => + ffmpegDetermineVideoDuration(pathOrZipItem), ); // - Upload @@ -233,9 +242,11 @@ export const attachIPCHandlers = () => { * the main window to do their thing. */ export const attachMainWindowIPCHandlers = (mainWindow: BrowserWindow) => { - // - ML + // - Utility processes - ipcMain.on("createMLWorker", () => createMLWorker(mainWindow)); + ipcMain.on("triggerCreateUtilityProcess", (_, type: UtilityProcessType) => + triggerCreateUtilityProcess(type, mainWindow), + ); }; /** diff --git a/desktop/src/main/log-worker.ts b/desktop/src/main/log-worker.ts new file mode 100644 index 0000000000..7f0eb7a066 --- /dev/null +++ b/desktop/src/main/log-worker.ts @@ -0,0 +1,59 @@ +/** + * A object that behaves similar to the default export of "./log", except this + * can be used from within a utility process. + * + * --- + * + * We cannot directly do + * + * import log from "../log"; + * + * because that requires the Electron APIs that are not available to a utility + * process (See: [Note: Using Electron APIs in UtilityProcess]). + * + * But even if that were to work, logging will still be problematic since we'd + * try opening the log file from two different Node.js processes (this one, and + * the main one), and I didn't find any indication in the electron-log + * repository that the log file's integrity would be maintained in such cases. + * + * So instead we provide this proxy log object that uses the + * `process.parentPort` to transport the logs over to the main process, where + * the {@link processUtilityProcessLogMessage} function in the main process is + * expected to handle these (sending them to the actual log). + */ +export default { + error: (s: string, e?: unknown) => + mainProcess("log.errorString", messageWithError(s, e)), + warn: (s: string, e?: unknown) => + mainProcess("log.warnString", messageWithError(s, e)), + info: (...ms: unknown[]) => mainProcess("log.info", ms), + /** + * Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b) + * accepts only strings. + */ + debugString: (s: string) => mainProcess("log.debugString", s), +}; + +/** + * Send a message to the main process using a barebones RPC protocol. + */ +const mainProcess = (method: string, param: unknown) => + process.parentPort.postMessage({ method, p: param }); + +// Duplicated verbatim from ./log.ts +const messageWithError = (message: string, e?: unknown) => { + if (!e) return message; + + let es: string; + if (e instanceof Error) { + // In practice, we expect ourselves to be called with Error objects, so + // this is the happy path so to say. + es = [`${e.name}: ${e.message}`, e.stack].filter((x) => x).join("\n"); + } else { + // For the rest rare cases, use the default string serialization of e. + // eslint-disable-next-line @typescript-eslint/no-base-to-string + es = String(e); + } + + return `${message}: ${es}`; +}; diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index 479b3c1666..5e165da0fd 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -83,6 +83,56 @@ const logDebug = (param: () => unknown) => { } }; +/** + * Handle log messages posted from the utility process in the main process. + * + * See: [Note: Using Electron APIs in UtilityProcess] + * + * @param message The arbitrary message that was received as an argument to the + * "message" event invoked on a {@link UtilityProcess}. + * + * @returns true if the message was recognized and handled, and false otherwise. + */ +export const processUtilityProcessLogMessage = ( + logTag: string, + message: unknown, +) => { + const m = message; /* shorter alias */ + if (m && typeof m == "object" && "method" in m && "p" in m) { + const p = m.p; + switch (m.method) { + case "log.errorString": + if (typeof p == "string") { + logError(`${logTag} ${p}`); + return true; + } + break; + case "log.warnString": + if (typeof p == "string") { + logWarn(`${logTag} ${p}`); + return true; + } + break; + case "log.info": + if (Array.isArray(p)) { + // Need to cast from any[] to unknown[] + logInfo(logTag, ...(p as unknown[])); + return true; + } + break; + case "log.debugString": + if (typeof p == "string") { + logDebug(() => `${logTag} ${p}`); + return true; + } + break; + default: + break; + } + } + return false; +}; + /** * Ente's logger. * diff --git a/desktop/src/main/services/ffmpeg-worker.ts b/desktop/src/main/services/ffmpeg-worker.ts new file mode 100644 index 0000000000..b63abcf023 --- /dev/null +++ b/desktop/src/main/services/ffmpeg-worker.ts @@ -0,0 +1,1133 @@ +/** + * @file ffmpeg invocations. This code runs in a utility process. + */ + +// See [Note: Using Electron APIs in UtilityProcess] about what we can and +// cannot import. +import shellescape from "any-shell-escape"; +import { expose } from "comlink"; +import pathToFfmpeg from "ffmpeg-static"; +import { randomBytes } from "node:crypto"; +import fs_ from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { z } from "zod/v4"; +import type { FFmpegCommand } from "../../types/ipc"; +import log from "../log-worker"; +import { messagePortMainEndpoint } from "../utils/comlink"; +import { nullToUndefined, wait } from "../utils/common"; +import { execAsyncWorker } from "../utils/exec-worker"; +import { + authenticatedRequestHeaders, + publicRequestHeaders, +} from "../utils/http"; + +/* Ditto in the web app's code (used by the Wasm FFmpeg invocation). */ +const ffmpegPathPlaceholder = "FFMPEG"; +const inputPathPlaceholder = "INPUT"; +const outputPathPlaceholder = "OUTPUT"; + +/** + * The interface of the object exposed by `ffmpeg-worker.ts` on the message port + * pair that the main process creates to communicate with it. + * + * @see {@link ffmpegUtilityProcessEndpoint}. + */ +export interface FFmpegUtilityProcess { + ffmpegExec: ( + command: FFmpegCommand, + inputFilePath: string, + outputFilePath: string, + ) => Promise; + + ffmpegConvertToMP4: ( + inputFilePath: string, + outputFilePath: string, + ) => Promise; + + ffmpegGenerateHLSPlaylistAndSegments: ( + inputFilePath: string, + outputPathPrefix: string, + fileID: number, + fetchURL: string, + authToken: string, + ) => Promise; + + ffmpegDetermineVideoDuration: (inputFilePath: string) => Promise; +} + +log.debugString("Started ffmpeg utility process"); + +process.on("uncaughtException", (e, origin) => log.error(origin, e)); + +process.parentPort.once("message", (e) => { + // Initialize ourselves with the data we got from our parent. + parseInitData(e.data); + // Expose an instance of `FFmpegUtilityProcess` on the port we got from our + // parent. + expose( + { + ffmpegExec, + ffmpegConvertToMP4, + ffmpegGenerateHLSPlaylistAndSegments, + ffmpegDetermineVideoDuration, + } satisfies FFmpegUtilityProcess, + messagePortMainEndpoint(e.ports[0]!), + ); + // Let the main process know we're ready. + mainProcess("ack", undefined); +}); + +/** + * We cannot access Electron's {@link app} object within a utility process, so + * we pass the value of `app.getVersion()` during initialization, and it can be + * subsequently retrieved from here. + */ +let _desktopAppVersion: string | undefined; + +/** Equivalent to `app.getVersion()` */ +const desktopAppVersion = () => _desktopAppVersion!; + +const FFmpegWorkerInitData = z.object({ appVersion: z.string() }); + +const parseInitData = (data: unknown) => { + _desktopAppVersion = FFmpegWorkerInitData.parse(data).appVersion; +}; + +/** + * Send a message to the main process using a barebones RPC protocol. + */ +const mainProcess = (method: string, param: unknown) => + process.parentPort.postMessage({ method, p: param }); + +/** + * Run a FFmpeg command + * + * [Note: FFmpeg in Electron] + * + * There is a Wasm build of FFmpeg, but that is currently 10-20 times slower + * that the native build. That is slow enough to be unusable for our purposes. + * https://ffmpegwasm.netlify.app/docs/performance + * + * So the alternative is to bundle a FFmpeg executable binary with our app. e.g. + * + * yarn add fluent-ffmpeg ffmpeg-static ffprobe-static + * + * (we only use ffmpeg-static, the rest are mentioned for completeness' sake). + * + * Interestingly, Electron already bundles an binary FFmpeg library (it comes + * from the ffmpeg fork maintained by Chromium). + * https://chromium.googlesource.com/chromium/third_party/ffmpeg + * https://stackoverflow.com/questions/53963672/what-version-of-ffmpeg-is-bundled-inside-electron + * + * This can be found in (e.g. on macOS) at + * + * $ file ente.app/Contents/Frameworks/Electron\ Framework.framework/Versions/Current/Libraries/libffmpeg.dylib + * .../libffmpeg.dylib: Mach-O 64-bit dynamically linked shared library arm64 + * + * But I'm not sure if our code is supposed to be able to use it, and how. + */ +const ffmpegExec = async ( + command: FFmpegCommand, + inputFilePath: string, + outputFilePath: string, +): Promise => { + let resolvedCommand: string[]; + if (Array.isArray(command)) { + resolvedCommand = command; + } else { + const isHDR = await isHDRVideo(inputFilePath); + resolvedCommand = isHDR ? command.hdr : command.default; + } + + const cmd = substitutePlaceholders( + resolvedCommand, + inputFilePath, + outputFilePath, + ); + + await execAsyncWorker(cmd); +}; + +const substitutePlaceholders = ( + command: string[], + inputFilePath: string, + outputFilePath: string, +) => + command.map((segment) => { + if (segment == ffmpegPathPlaceholder) { + return ffmpegBinaryPath(); + } else if (segment == inputPathPlaceholder) { + return inputFilePath; + } else if (segment == outputPathPlaceholder) { + return outputFilePath; + } else { + return segment; + } + }); + +/** + * Return the path to the `ffmpeg` binary. + * + * At runtime, the FFmpeg binary is present in a path like (macOS example): + * `ente.app/Contents/Resources/app.asar.unpacked/node_modules/ffmpeg-static/ffmpeg` + */ +const ffmpegBinaryPath = () => { + // This substitution of app.asar by app.asar.unpacked is suggested by the + // ffmpeg-static library author themselves: + // https://github.com/eugeneware/ffmpeg-static/issues/16 + return pathToFfmpeg!.replace("app.asar", "app.asar.unpacked"); +}; + +/** + * A variant of {@link ffmpegExec} adapted to work with streams so that it can + * handle the MP4 conversion of large video files. + * + * @param inputFilePath The path to a file on the user's local file system. This + * is the video we want to convert. + * + * @param outputFilePath The path to a file on the user's local file system where + * we should write the converted MP4 video. + */ +const ffmpegConvertToMP4 = async ( + inputFilePath: string, + outputFilePath: string, +): Promise => { + const command = [ + ffmpegPathPlaceholder, + "-i", + inputPathPlaceholder, + "-preset", + "ultrafast", + outputPathPlaceholder, + ]; + + const cmd = substitutePlaceholders(command, inputFilePath, outputFilePath); + + await execAsyncWorker(cmd); +}; + +export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { + playlistPath: string; + dimensions: { width: number; height: number }; + videoSize: number; + videoObjectID: string; +} + +/** + * A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for + * videos. + * + * Overview of the cases: + * + * H.264, <= 10 MB - Skip + * H.264, <= 4000 kb/s bitrate - Don't re-encode video stream + * !HDR, <= 2000 kb/s bitrate - Don't apply the scale+fps filter + * HDR - Apply tonemap (zscale+tonemap+zscale) + * + * Example invocation: + * + * ffmpeg -i in.mov -vf "scale=-2:'min(720,ih)',fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p" -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8 + * + * See: [Note: Preview variant of videos] + * + * @param inputFilePath The path to a file on the user's local file system. This + * is the video we want to generate an streamable HLS playlist for. + * + * @param outputPathPrefix The path to unique, unused and temporary prefix on + * the user's local file system. This function will write the generated HLS + * playlist and video segments under this prefix. + * + * @param fileID The ID of the {@link EnteFile} whose HLS playlist we are + * generating. + * + * @param fetchURL The fully resolved API URL for obtaining pre-signed S3 URLs + * for uploading the generated video segment file. + * + * @param authToken A token that can be used to make API request to + * {@link fetchURL}. + * + * @returns The path to the file on the user's file system containing the + * generated HLS playlist, and other metadata about the generated video stream. + * + * If the video is such that it doesn't require stream generation, then this + * function returns `undefined`. + */ +const ffmpegGenerateHLSPlaylistAndSegments = async ( + inputFilePath: string, + outputPathPrefix: string, + fileID: number, + fetchURL: string, + authToken: string, +): Promise => { + const { isH264, isHDR, bitrate } = + await detectVideoCharacteristics(inputFilePath); + + log.debugString(JSON.stringify({ isH264, isHDR, bitrate })); + + // If the video is smaller than 10 MB, and already H.264 (the codec we are + // going to use for the conversion), then a streaming variant is not much + // use. Skip such cases. + // + // See also: [Note: Marking files which do not need video processing] + // + // --- + // + // [Note: HEVC/H.265 issues] + // + // We've observed two issues out in the wild with HEVC videos: + // + // 1. On Linux, HEVC video streams don't play. However, since the audio + // stream plays, the browser tells us that the "video" itself is + // playable, but the user sees a blank screen with only audio. + // + // 2. HEVC + HDR videos taken on an iPhone have a rotation (`Side data: + // displaymatrix` in the ffmpeg output) that Chrome (and thus Electron) + // doesn't take into account, so these play upside down. + // + // Not fully related to this case, but mentioning here as to why both the + // size and codec need to be checked before skipping stream generation. + if (isH264) { + const inputVideoSize = await fs + .stat(inputFilePath) + .then((st) => st.size); + if (inputVideoSize <= 10 * 1024 * 1024 /* 10 MB */) { + return undefined; + } + } + + // If the video is already H.264 with a bitrate less than 4000 kbps, then we + // do not need to reencode the video stream (by _far_ the costliest part of + // the HLS stream generation). + const reencodeVideo = !(isH264 && bitrate && bitrate <= 4000 * 1000); + + // If the bitrate is not too high, then we don't need to rescale the video + // when generating the video stream. This is not a performance optimization, + // but more for avoiding making the video size smaller unnecessarily. + const rescaleVideo = !(bitrate && bitrate <= 2000 * 1000); + + // [Note: Tonemapping HDR to HD] + // + // BT.709 ("HD") is a standard that describes things like how color is + // encoded, the range of values, and their "meaning" - i.e. how to map the + // values in the video to the pixels on the screen. + // + // It is not the only such standard, there are three common examples: + // + // - BT.601 ("Standard-Definition" or SD) + // - BT.709 ("High-Definition" or HD) + // - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^). + // + // ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for the + // discussion here we can treat it as as alias. In particular, not all + // BT.2020 videos are HDR, the check we use instead looks for particular + // color transfers (see the `isHDRVideo` function below). + // + // BT.709 is the most common amongst these for older files out stored on + // computers, and they conform mostly to the standard (one notable exception + // is that the BT.709 standard also recommends using the yuv422p pixel + // format, but de facto yuv420p is used because many video players only + // support yuv420p). + // + // Since BT.709 is the most widely supported standard, we use it when + // generating the HLS playlist so to allow playback across the widest + // possible hardware/OS/browser combinations. + // + // If we convert HDR to HD without naively, then the colors look washed out + // compared to the original. To resolve this, we use a ffmpeg filterchain + // that uses the tonemap filter. + // + // However applying this tonemap to videos that are already HD leads to a + // brightness drop. So we conditionally apply this filter chain only if we + // can heuristically detect that the video is HDR. + // + // See also: [Note: Alternative FFmpeg command for HDR videos]. + // + // Reference: + // - https://trac.ffmpeg.org/wiki/colorspace + const tonemap = isHDR; + + // We want the generated playlist to refer to the chunks as "output.ts". + // + // So we arrange things accordingly: We use the `outputPathPrefix` as our + // working directory, and then ask ffmpeg to generate a playlist with the + // name "output.m3u8". + // + // ffmpeg will automatically place the segments in a file with the same base + // name as the playlist, but with a ".ts" extension. And since we use the + // "single_file" option, all the segments will be placed in a file named + // "output.ts". + + await fs.mkdir(outputPathPrefix); + + const playlistPath = path.join(outputPathPrefix, "output.m3u8"); + const videoPath = path.join(outputPathPrefix, "output.ts"); + + // A file into which we'll redirect ffmpeg's stderr. + // + // [Note: ERR_CHILD_PROCESS_STDIO_MAXBUFFER] + // + // For very large videos, the stderr output of ffmpeg may cause the stdio + // max buffer size limits to be exceeded, raising the following error: + // + // RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stderr maxBuffer length exceeded + // + // So instead of capturing the stderr normally, we redirect it to a + // temporary file, and then read it from there to extract the video + // dimensions. + const stderrPath = path.join(outputPathPrefix, "stderr.txt"); + + // Generate a cryptographically secure random key (16 bytes). + const keyBytes = randomBytes(16); + const keyB64 = keyBytes.toString("base64"); + + // Convert it to a data: URI that will be added to the playlist. + const keyURI = `data:text/plain;base64,${keyB64}`; + + // Determine two paths - one where we will write the key itself, and where + // we will write the "key info" that provides ffmpeg the `keyURI` and the + // `keyPath;. + const keyPath = playlistPath + ".key"; + const keyInfoPath = playlistPath + ".key-info"; + + // Generate a "key info": + // + // - the first line specifies the key URI that is written into the playlist. + // - the second line specifies the path to the local file system file from + // where ffmpeg should read the key. + // + // [Note: ffmpeg newlines] + // + // Tested on Windows that ffmpeg recognizes these lines correctly. In + // general, ffmpeg tends to expect input and write output the Unix way (\n), + // even when we're running on Windows. + // + // - The ffmetadata and the HLS playlist file generated by ffmpeg uses \n + // separators, even on Windows. + // - The HLS key info file, expected as an input by ffmpeg, works fine when + // \n separated even on Windows. + // + const keyInfo = [keyURI, keyPath].join("\n"); + + // Overview: + // + // - Video H.264 HD 720p (max) 30fps. + // - Audio AAC 128kbps. + // - Encrypted HLS playlist with a single file containing all the chunks. + // + // Reference: + // - `man ffmpeg-all` + // - https://trac.ffmpeg.org/wiki/Encode/H.264 + // + const command = [ + ffmpegBinaryPath(), + // Reduce the amount of output lines we have to parse. + ["-hide_banner"], + // Input file. We don't need any extra options that apply to the input file. + "-i", + inputFilePath, + // The remaining options apply to the next output file (`playlistPath`). + reencodeVideo + ? [ + // `-vf` creates a filter graph for the video stream. It is a + // comma separated list of filters chained together, e.g. + // `filter1=key=value:key=value.filter2=key=value`. + "-vf", + [ + // Do the rescaling to even number of pixels always if the + // tonemapping is going to be applied subsequently, + // otherwise the tonemapping will fail with "image + // dimensions must be divisible by subsampling factor". + // + // While we add the extra condition here for completeness, + // it won't usually matter since a non-BT.709 video is + // likely using a new codec, and as such would've a high + // enough bitrate to require rescaling anyways. + rescaleVideo || tonemap + ? [ + // Scales the video to maximum 720p height, + // keeping aspect ratio and the calculated + // dimension divisible by 2 (some of the other + // operations require an even pixel count). + "scale=-2:'min(720,ih)'", + // Convert the video to a constant 30 fps, + // duplicating or dropping frames as necessary. + "fps=30", + ] + : [], + // Convert the colorspace if the video is HDR. Before + // conversion, tone map colors so that they work the same + // across the change in the dyamic range. + // + // 1. The tonemap filter only works linear light, so we + // first use zscale with transfer=linear to linearize + // the input. + // + // 2. Then we use the tonemap, with the hable option that + // is best for preserving details. desat=0 turns off + // the default desaturation. + // + // 3. Use zscale again to "convert to BT.709" by asking it + // to set the all three of color primaries, transfer + // characteristics and colorspace matrix to 709 (Note: + // the constants specified in the tonemap filter help + // do not include the "bt" prefix) + // + // See: https://ffmpeg.org/ffmpeg-filters.html#tonemap-1 + // + // See: [Note: Tonemapping HDR to HD] + tonemap + ? [ + "zscale=transfer=linear", + "tonemap=tonemap=hable:desat=0", + "zscale=primaries=709:transfer=709:matrix=709", + ] + : [], + // Output using the well supported pixel format: 8-bit YUV + // planar color space with 4:2:0 chroma subsampling. + "format=yuv420p", + ] + .flat() + .join(","), + ] + : [], + reencodeVideo + ? // Video codec H.264 + // + // - `-c:v libx264` converts the video stream to the H.264 codec. + // + // - We don't supply a bitrate, instead it uses the default CRF + // ("23") as recommended in the ffmpeg trac. + // + // - We don't supply a preset, it'll use the default ("medium"). + ["-c:v", "libx264"] + : // Keep the video stream unchanged + ["-c:v", "copy"], + // Audio codec AAC + // + // - `-c:a aac` converts the audio stream to use the AAC codec + // + // - We don't supply a bitrate, it'll use the AAC default 128k bps. + ["-c:a", "aac"], + // Generate a HLS playlist. + ["-f", "hls"], + // Tell ffmpeg where to find the key, and the URI for the key to write + // into the generated playlist. Implies "-hls_enc 1". + ["-hls_key_info_file", keyInfoPath], + // Generate as many playlist entries as needed (default limit is 5). + ["-hls_list_size", "0"], + // Place all the video segments within the same .ts file (with the same + // path as the playlist file but with a ".ts" extension). + ["-hls_flags", "single_file"], + // Output path where the playlist should be generated. + playlistPath, + ].flat(); + + let dimensions: { width: number; height: number }; + let videoSize: number; + let videoObjectID: string; + + try { + // Write the key and the keyInfo to their desired paths. + await Promise.all([ + fs.writeFile(keyPath, keyBytes), + fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }), + ]); + + // Tack on the redirection after constructing the command. + const commandWithRedirection = `${shellescape(command)} 2>${stderrPath}`; + + // Run the ffmpeg command to generate the HLS playlist and segments. + // + // Note: Depending on the size of the input file, this may take long! + await execAsyncWorker(commandWithRedirection); + + // While ffmpeg uses \n as the line separator in the generated playlist + // file on Windows too, add an extra safety check that should fail the + // HLS generation if this doesn't hold. See: [Note: ffmpeg newlines]. + if (process.platform == "win32") { + const playlistText = await fs.readFile(playlistPath, "utf-8"); + if (playlistText.includes("\r\n")) + throw new Error("Unexpected Windows newlines in playlist"); + } + + // Determine the dimensions of the generated video from the stderr + // output produced by ffmpeg during the conversion. + dimensions = await detectVideoDimensions(stderrPath); + + // Find the size of the generated video segments by reading the size of + // the generated .ts file. + videoSize = await fs.stat(videoPath).then((st) => st.size); + + videoObjectID = await uploadVideoSegments( + videoPath, + videoSize, + fileID, + fetchURL, + authToken, + ); + } catch (e) { + log.error("HLS generation failed", e); + await Promise.all([deletePathIgnoringErrors(playlistPath)]); + throw e; + } finally { + await Promise.all([ + deletePathIgnoringErrors(stderrPath), + deletePathIgnoringErrors(keyInfoPath), + deletePathIgnoringErrors(keyPath), + deletePathIgnoringErrors(videoPath), + // ffmpeg writes a /path/output.ts.tmp, clear it out too. + deletePathIgnoringErrors(videoPath + ".tmp"), + ]); + } + + return { playlistPath, dimensions, videoSize, videoObjectID }; +}; + +/** + * A variant of {@link deletePathIgnoringErrors} (which we can't directly use in + * the utility process). It unconditionally removes the item at the provided + * path; in particular, this will not raise any errors if there is no item at + * the given path (as may be expected to happen when we run during catch + * handlers). + */ +const deletePathIgnoringErrors = async (tempFilePath: string) => { + try { + await fs.rm(tempFilePath, { force: true }); + } catch (e) { + log.error(`Could not delete item at path ${tempFilePath}`, e); + } +}; + +/** + * A regex that matches the first line of the form + * + * Stream #0:0: Video: h264 (High 10) ([27][0][0][0] / 0x001B), yuv420p10le(tv, bt2020nc/bt2020/arib-std-b67), 1920x1080, 30 fps, 30 tbr, 90k tbn + * + * The part after Video: is the first capture group. + * + * Another example: + * + * Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default) + */ +const videoStreamLineRegex = /Stream #.+: Video:(.+)\r?\n/; + +/** {@link videoStreamLineRegex}, but global. */ +const videoStreamLinesRegex = /Stream #.+: Video:(.+)\r?\n/g; + +/** + * A regex that matches " kb/s" preceded by a space. See + * {@link videoStreamLineRegex} for the context in which it is used. + */ +const videoBitrateRegex = / ([1-9]\d*) kb\/s/; + +/** + * A regex that matches x pair preceded by a space. See + * {@link videoStreamLineRegex} for the context in which it is used. + * + * We constrain the digit sequence not to begin with 0 to exclude hexadecimal + * representations of various constants that ffmpeg prints on this line (e.g. + * "avc1 / 0x31637661"). + */ +const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/; + +interface VideoCharacteristics { + isH264: boolean; + isHDR: boolean; + bitrate: number | undefined; +} + +/** + * Heuristically determine information about the video at the given + * {@link inputFilePath}: + * + * - If is encoded using H.264 codec. + * - If it is HDR. + * - Its bitrate. + * + * The defaults are tailored for the cases in which these conditions are used, + * so that even if we get the detection wrong we'll only end up encoding videos + * that could've possibly been skipped as an optimization. + * + * [Note: Parsing CLI output might break on ffmpeg updates] + * + * This function tries to determine the these bits of information about the + * given video by scanning the ffmpeg info output for the video stream line, and + * doing various string matches and regex extractions. + * + * Needless to say, while this works currently, this is liable to break in the + * future. So if something stops working after updating ffmpeg, look here! + * + * Ideally, we'd have done this using `ffprobe`, but we don't have the ffprobe + * binary at hand, so we make do by grepping the log output of ffmpeg. + * + * For reference, + * + * - codec and colorspace are printed by the `avcodec_string` function in the + * ffmpeg source: + * https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.c + * + * - bitrate is printed by the `dump_stream_format` function in `dump.c`. + */ +const detectVideoCharacteristics = async (inputFilePath: string) => { + const videoInfo = await pseudoFFProbeVideo(inputFilePath); + const videoStreamLine = videoStreamLineRegex.exec(videoInfo)?.at(1)?.trim(); + + // Since the checks are heuristic, start with defaults that would cause the + // codec conversion to happen, even if it is unnecessary. + const res: VideoCharacteristics = { + isH264: false, + isHDR: false, + bitrate: undefined, + }; + if (!videoStreamLine) return res; + + res.isH264 = videoStreamLine.startsWith("h264 "); + + // Same check as `isHDRVideo`. + res.isHDR = + videoStreamLine.includes("smpte2084") || + videoStreamLine.includes("arib-std-b67"); + + // The regex matches "\d kb/s", but there can be other units for the + // bitrate. However, (a) "kb/s" is the most common for videos out in the + // wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead + // of "-v:c copy", so only unnecessary processing but no change in output. + const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0); + if (brs) { + const br = parseInt(brs, 10); + if (br) res.bitrate = br * 1000; + } + + return res; +}; + +/** + * Heuristically detect the dimensions of the given video from the log output of + * the ffmpeg invocation during the HLS playlist generation. + * + * This function tries to determine the width and height of the generated video + * from the output log written by ffmpeg on its stderr during the generation + * process, scanning it for the last video stream line, and trying to match a + * "x" regex. + * + * See: [Note: Parsing CLI output might break on ffmpeg updates]. + */ +const detectVideoDimensions = async (stderrPath: string) => { + // Instead of reading the stderr directly off the child_process.exec, we + // wrote it to a file to avoid hitting the max stdio buffer limits. Read it + // from there. + const conversionStderr = await fs.readFile(stderrPath, "utf-8"); + + // There is a nicer way to do it - by running `pseudoFFProbeVideo` on the + // generated playlist. However, that playlist includes a data URL that + // specifies the encryption info, and ffmpeg refuses to read that unless we + // specify the "-allowed_extensions ALL" or something to that effect. + // + // Unfortunately, our current ffmpeg binary (5.x) does not support that + // option. So we instead parse the conversion output itself. + // + // This is also nice, since it saves on an extra ffmpeg invocation. But we + // now need to be careful to find the right video stream line, since the + // conversion output includes both the input and output video stream lines. + // + // To match the right (output) video stream line, we use a global regex, and + // use the last match since that'd correspond to the single video stream + // written in the output. + const videoStreamLine = Array.from( + conversionStderr.matchAll(videoStreamLinesRegex), + ) + .at(-1) /* Last Stream...: Video: line in the output */ + ?.at(1); /* First capture group */ + if (videoStreamLine) { + const [, ws, hs] = videoDimensionsRegex.exec(videoStreamLine) ?? []; + if (ws && hs) { + const w = parseInt(ws, 10); + const h = parseInt(hs, 10); + if (w && h) { + return { width: w, height: h }; + } + } + } + throw new Error( + `Unable to detect video dimensions from stream line [${videoStreamLine ?? ""}]`, + ); +}; + +/** + * Heuristically detect if the file at given path is a HDR video. + * + * This is similar to {@link detectVideoCharacteristics}, and see that + * function's documentation for all the caveats. Specifically, this function + * uses an allow-list, and considers any file with color transfer "smpte2084" or + * "arib-std-b67" to be HDR. Caveats: + * + * 1. These particular constants are not guaranteed to be correct; these are + * from various internet posts as being used / recommended for detecting HDR. + * + * 2. Since we don't have ffprobe, we're not checking the color space value + * itself but a substring of the stream line in the ffmpeg stderr output. + * + * This check should generally not have false positives (unless something else + * in the log line triggers #2), but it can have false negative. This is the + * lesser of the two evils since if we apply the tonemapping to any non-BT.709 + * file, we start getting the "code 3074: no path between colorspaces" error + * during the JPEG or H.264 conversion. + * + * - See: [Note: Alternative FFmpeg command for HDR videos] + * - See: [Note: Tonemapping HDR to HD] + * + * @param inputFilePath The path to a video file on the user's machine. + * + * @returns `true` if this file is likely a HDR video. Exceptions are treated as + * `false` to make this function safe to invoke without breaking the happy path. + */ +const isHDRVideo = async (inputFilePath: string) => { + try { + const videoInfo = await pseudoFFProbeVideo(inputFilePath); + const vs = videoStreamLineRegex.exec(videoInfo)?.at(1); + if (!vs) return false; + return vs.includes("smpte2084") || vs.includes("arib-std-b67"); + } catch (e) { + log.warn(`Could not detect HDR status of ${inputFilePath}`, e); + return false; + } +}; + +/** + * Return the stderr of ffmpeg in an attempt to gain information about the video + * at the given {@link inputFilePath}. + * + * We don't have the ffprobe binary at hand, which is why we need to use this + * alternative. See: [Note: Parsing CLI output might break on ffmpeg updates] + * + * @returns the stderr of ffmpeg after running it on the input file. The exact + * command we run is: + * + * ffmpeg -i in.mov -an -frames:v 0 -f null - 2>info.txt + * + * And the returned string is the contents of the `info.txt` thus produced. + */ +const pseudoFFProbeVideo = async (inputFilePath: string) => { + const command = [ + ffmpegPathPlaceholder, + // Reduce the amount of output lines we have to parse. + ["-hide_banner"], + ["-i", inputPathPlaceholder], + "-an", + ["-frames:v", "0"], + ["-f", "null"], + "-", + ].flat(); + + const cmd = substitutePlaceholders(command, inputFilePath, /* NA */ ""); + + const { stderr } = await execAsyncWorker(cmd); + + return stderr; +}; + +/** + * Upload the file at the given {@link videoFilePath} to the provided pre-signed + * URL(s) using a HTTP PUT request. + * + * All HTTP requests are retried up to 4 times (1 original + 3 retries) with + * exponential backoff. + * + * See: [Note: Upload HLS video segment from node side]. + * + * @param videoFilePath The path to the file on the user's file system to + * upload. + * + * @param videoSize The size in bytes of the file at {@link videoFilePath}. + * + * @param fileID The ID of the {@link EnteFile} whose video segment this is. + * + * @param fetchURL The API URL for fetching pre-signed upload URLs. + * + * @param authToken The user's auth token for use with {@link fetchURL}. + * + * @return The object ID of the uploaded file on remote storage. + */ +const uploadVideoSegments = async ( + videoFilePath: string, + videoSize: number, + fileID: number, + fetchURL: string, + authToken: string, +) => { + // Self hosters might be using Cloudflare's free plan which (currently) has + // a maximum request size of 100 MB. Keeping a bit of margin for headers, + const partSize = 96 * 1024 * 1024; /* 96 MB */ + const partCount = Math.ceil(videoSize / partSize); + + const { objectID, url, partURLs, completeURL } = + await getFilePreviewDataUploadURL( + partCount, + fileID, + fetchURL, + authToken, + ); + + if (url) { + await uploadVideoSegmentsSingle(videoFilePath, videoSize, url); + } else if (partURLs && completeURL) { + await uploadVideoSegmentsMultipart( + videoFilePath, + videoSize, + partSize, + partURLs, + completeURL, + ); + } else { + throw new Error("Malformed upload URLs"); + } + + return objectID; +}; + +const FilePreviewDataUploadURLResponse = z.object({ + /** + * The objectID with which this uploaded data can be referred to post upload + * (e.g. when invoking {@link putVideoData}). + */ + objectID: z.string(), + /** + * A pre-signed URL that can be used to upload the file. + * + * This will be present only if we requested a singular object upload URL. + */ + url: z.string().nullish().transform(nullToUndefined), + /** + * A list of pre-signed URLs that can be used to upload parts of a multipart + * upload of the uploaded data. + * + * This will be present only if we requested a multipart upload URLs for the + * object by setting `isMultiPart` true in the request. + */ + partURLs: z.string().array().nullish().transform(nullToUndefined), + /** + * A pre-signed URL that can be used to finalize the multipart upload. + * + * This will be present only if we requested a multipart upload URLs for the + * object by setting `isMultiPart` true in the request. + */ + completeURL: z.string().nullish().transform(nullToUndefined), +}); + +/** + * Obtain a pre-signed URL(s) that can be used to upload the "file preview data" + * of type "vid_preview". + * + * This will be the file containing the encrypted video segments which the + * "vid_preview" HLS playlist for the file would refer to. + * + * @param partCount If greater than 1, then we request for a multipart upload. + */ +export const getFilePreviewDataUploadURL = async ( + partCount: number, + fileID: number, + fetchURL: string, + authToken: string, +) => { + const params = new URLSearchParams({ + fileID: fileID.toString(), + type: "vid_preview", + }); + if (partCount > 1) { + params.set("isMultiPart", "true"); + params.set("count", partCount.toString()); + } + + const res = await retryEnsuringHTTPOk(() => + fetch(`${fetchURL}?${params.toString()}`, { + headers: authenticatedRequestHeaders( + desktopAppVersion(), + authToken, + ), + }), + ); + + return FilePreviewDataUploadURLResponse.parse(await res.json()); +}; + +const uploadVideoSegmentsSingle = ( + videoFilePath: string, + videoSize: number, + objectUploadURL: string, +) => + retryEnsuringHTTPOk(() => + // net.fetch is 40-50x slower than the native fetch for this particular + // PUT request. This is easily reproducible - replace `fetch` with + // `net.fetch`, then even on localhost the PUT requests start taking a + // minute or so, while they take second(s) with node's native fetch. + fetch(objectUploadURL, { + method: "PUT", + // net.fetch deduces and inserts a content-length for us, when we + // use the node native fetch then we need to provide it explicitly. + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Length": `${videoSize}`, + }, + // See: [Note: duplex param required for stream body] + // @ts-expect-error ^see note above + duplex: "half", + body: Readable.toWeb(fs_.createReadStream(videoFilePath)), + }), + ); + +/** + * Retry a async operation on failure up to 4 times (1 original + 3 retries) + * with exponential backoff. + * + * This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOk` from + * `web/packages/base/http.ts` + * + * - We don't have the rest of the scaffolding used by that function, which is + * why it is intially inlined bespoked. + * + * - It handles the specific use case of uploading videos since generating the + * HLS stream is a fairly expensive operation, so a retry to discount + * transient network issues is called for. The number of retries and their + * gaps are same as the "background" `retryProfile` of the web implementation. + * + * - Later it was discovered that net.fetch is much slower than node's native + * fetch, so this implementation has further diverged. + * + * - This also moved to a utility process, where we also have a more restricted + * ability to import electron API. + */ +const retryEnsuringHTTPOk = async (request: () => Promise) => { + const waitTimeBeforeNextTry = [10000, 30000, 120000]; + + while (true) { + try { + const res = await request(); + if (res.ok) /* Success. */ return res; + throw new Error( + `Request failed: HTTP ${res.status} ${res.statusText}`, + ); + } catch (e) { + const t = waitTimeBeforeNextTry.shift(); + if (!t) { + throw e; + } else { + log.warn("Will retry potentially transient request failure", e); + await wait(t); + } + } + } +}; + +const uploadVideoSegmentsMultipart = async ( + videoFilePath: string, + videoSize: number, + partSize: number, + partUploadURLs: string[], + completionURL: string, +) => { + // The part we're currently uploading. + let partNumber = 0; + // A rolling offset into the file. + let start = 0; + // See `createMultipartUploadRequestBody` in the web code for a more + // expansive and documented version of this XML body construction. + const completionXML = [""]; + for (const partUploadURL of partUploadURLs) { + partNumber += 1; + const size = Math.min(start + partSize, videoSize) - start; + const end = start + size - 1; + const res = await retryEnsuringHTTPOk(() => + fetch(partUploadURL, { + method: "PUT", + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Length": `${size}`, + }, + // See: [Note: duplex param required for stream body] + // @ts-expect-error ^see note above + duplex: "half", + body: Readable.toWeb( + // start and end are inclusive 0-indexed range of bytes to + // read from the file. + fs_.createReadStream(videoFilePath, { start, end }), + ), + }), + ); + const eTag = res.headers.get("etag"); + if (!eTag) throw new Error("Response did not have an ETag"); + start += size; + completionXML.push( + `${partNumber}${eTag}`, + ); + } + completionXML.push(""); + const completionBody = completionXML.join(""); + return await retryEnsuringHTTPOk(() => + fetch(completionURL, { + method: "POST", + headers: { + ...publicRequestHeaders(desktopAppVersion()), + "Content-Type": "text/xml", + }, + body: completionBody, + }), + ); +}; + +/** + * A regex that matches the first line of the form + * + * Duration: 00:00:03.13, start: 0.000000, bitrate: 16088 kb/s + * + * The part after Duration: and until the first non-digit or colon is the first + * capture group, while after the dot is an optional second capture group. + */ +const videoDurationLineRegex = /\s\sDuration: ([0-9:]+)(.[0-9]+)?/; + +/** + * Determine the duration of the video at the given {@link inputFilePath}. + * + * While the detection works for all known cases, it is still heuristic because + * it uses ffmpeg output instead of ffprobe (which we don't have access to). + * See: [Note: Parsing CLI output might break on ffmpeg updates]. + */ +export const ffmpegDetermineVideoDuration = async (inputFilePath: string) => { + const videoInfo = await pseudoFFProbeVideo(inputFilePath); + const matches = videoDurationLineRegex.exec(videoInfo); + + const fail = () => { + throw new Error(`Cannot parse video duration '${matches?.at(0)}'`); + }; + + // The HH:mm:ss. + const ints = (matches?.at(1) ?? "") + .split(":") + .map((s) => parseInt(s, 10) || 0); + let [h, m, s] = [0, 0, 0]; + switch (ints.length) { + case 1: + s = ints[0]!; + break; + case 2: + m = ints[0]!; + s = ints[1]!; + break; + case 3: + h = ints[0]!; + m = ints[1]!; + s = ints[2]!; + break; + default: + fail(); + } + + // Optional subseconds. + const ss = parseFloat(`0${matches?.at(2) ?? ""}`); + + // Follow the same round up behaviour that the web side uses. + const duration = Math.ceil(h * 3600 + m * 60 + s + ss); + if (!duration) fail(); + return duration; +}; diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index b5587b6ba1..f64a721a05 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -1,678 +1,77 @@ -import pathToFfmpeg from "ffmpeg-static"; -import { randomBytes } from "node:crypto"; +/** + * @file A bridge to the ffmpeg utility process. This code runs in the main + * process. + */ + +import { wrap } from "comlink"; import fs from "node:fs/promises"; -import path, { basename } from "node:path"; import type { FFmpegCommand, ZipItem } from "../../types/ipc"; -import log from "../log"; -import { execAsync } from "../utils/electron"; import { deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "../utils/temp"; - -/* Ditto in the web app's code (used by the Wasm FFmpeg invocation). */ -const ffmpegPathPlaceholder = "FFMPEG"; -const inputPathPlaceholder = "INPUT"; -const outputPathPlaceholder = "OUTPUT"; +import type { FFmpegUtilityProcess } from "./ffmpeg-worker"; +import { ffmpegUtilityProcessEndpoint } from "./workers"; /** - * Run a FFmpeg command - * - * [Note: FFmpeg in Electron] - * - * There is a Wasm build of FFmpeg, but that is currently 10-20 times slower - * that the native build. That is slow enough to be unusable for our purposes. - * https://ffmpegwasm.netlify.app/docs/performance - * - * So the alternative is to bundle a FFmpeg executable binary with our app. e.g. - * - * yarn add fluent-ffmpeg ffmpeg-static ffprobe-static - * - * (we only use ffmpeg-static, the rest are mentioned for completeness' sake). - * - * Interestingly, Electron already bundles an binary FFmpeg library (it comes - * from the ffmpeg fork maintained by Chromium). - * https://chromium.googlesource.com/chromium/third_party/ffmpeg - * https://stackoverflow.com/questions/53963672/what-version-of-ffmpeg-is-bundled-inside-electron - * - * This can be found in (e.g. on macOS) at - * - * $ file ente.app/Contents/Frameworks/Electron\ Framework.framework/Versions/Current/Libraries/libffmpeg.dylib - * .../libffmpeg.dylib: Mach-O 64-bit dynamically linked shared library arm64 - * - * But I'm not sure if our code is supposed to be able to use it, and how. + * Return a handle to the ffmpeg utility process, starting it if needed. + */ +export const ffmpegUtilityProcess = () => + ffmpegUtilityProcessEndpoint().then((port) => + wrap(port), + ); + +/** + * Implement the IPC "ffmpegExec" contract, writing the input and output to + * temporary files as needed, and then forward to the {@link ffmpegExec} running + * in the utility process. */ export const ffmpegExec = async ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, -): Promise => { +): Promise => + withInputFile(pathOrZipItem, async (worker, inputFilePath) => { + const outputFilePath = await makeTempFilePath(outputFileExtension); + try { + await worker.ffmpegExec(command, inputFilePath, outputFilePath); + return await fs.readFile(outputFilePath); + } finally { + await deleteTempFileIgnoringErrors(outputFilePath); + } + }); + +export const withInputFile = async ( + pathOrZipItem: string | ZipItem, + f: (worker: FFmpegUtilityProcess, inputFilePath: string) => Promise, +): Promise => { + const worker = await ffmpegUtilityProcess(); + const { path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem); + } = await makeFileForStreamOrPathOrZipItem(pathOrZipItem); - const outputFilePath = await makeTempFilePath(outputFileExtension); try { await writeToTemporaryInputFile(); - let resolvedCommand: string[]; - if (Array.isArray(command)) { - resolvedCommand = command; - } else { - const isHDR = await isHDRVideo(inputFilePath); - log.debug(() => [basename(inputFilePath), { isHDR }]); - resolvedCommand = isHDR ? command.hdr : command.default; - } - - const cmd = substitutePlaceholders( - resolvedCommand, - inputFilePath, - outputFilePath, - ); - - await execAsync(cmd); - - return await fs.readFile(outputFilePath); + return await f(worker, inputFilePath); } finally { if (isInputFileTemporary) await deleteTempFileIgnoringErrors(inputFilePath); - await deleteTempFileIgnoringErrors(outputFilePath); } }; -const substitutePlaceholders = ( - command: string[], - inputFilePath: string, - outputFilePath: string, -) => - command.map((segment) => { - if (segment == ffmpegPathPlaceholder) { - return ffmpegBinaryPath(); - } else if (segment == inputPathPlaceholder) { - return inputFilePath; - } else if (segment == outputPathPlaceholder) { - return outputFilePath; - } else { - return segment; - } - }); - /** - * Return the path to the `ffmpeg` binary. - * - * At runtime, the FFmpeg binary is present in a path like (macOS example): - * `ente.app/Contents/Resources/app.asar.unpacked/node_modules/ffmpeg-static/ffmpeg` + * Implement the IPC "ffmpegDetermineVideoDuration" contract, writing the input + * to temporary files as needed, and then forward to the + * {@link ffmpegDetermineVideoDuration} running in the utility process. */ -const ffmpegBinaryPath = () => { - // This substitution of app.asar by app.asar.unpacked is suggested by the - // ffmpeg-static library author themselves: - // https://github.com/eugeneware/ffmpeg-static/issues/16 - return pathToFfmpeg!.replace("app.asar", "app.asar.unpacked"); -}; - -/** - * A variant of {@link ffmpegExec} adapted to work with streams so that it can - * handle the MP4 conversion of large video files. - * - * @param inputFilePath The path to a file on the user's local file system. This - * is the video we want to convert. - * - * @param outputFilePath The path to a file on the user's local file system where - * we should write the converted MP4 video. - */ -export const ffmpegConvertToMP4 = async ( - inputFilePath: string, - outputFilePath: string, -): Promise => { - const command = [ - ffmpegPathPlaceholder, - "-i", - inputPathPlaceholder, - "-preset", - "ultrafast", - outputPathPlaceholder, - ]; - - const cmd = substitutePlaceholders(command, inputFilePath, outputFilePath); - - await execAsync(cmd); -}; - -export interface FFmpegGenerateHLSPlaylistAndSegmentsResult { - playlistPath: string; - videoPath: string; - dimensions: { width: number; height: number }; - videoSize: number; -} - -/** - * A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for - * videos. - * - * Overview of the cases: - * - * H.264, <= 10 MB - Skip - * H.264, <= 4000 kb/s bitrate - Don't re-encode video stream - * BT.709, <= 2000 kb/s bitrate - Don't apply the scale+fps filter - * !BT.709 - Apply tonemap (zscale+tonemap+zscale) - * - * Example invocation: - * - * ffmpeg -i in.mov -vf 'scale=-2:720,fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p' -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8 - * - * See: [Note: Preview variant of videos] - * - * @param inputFilePath The path to a file on the user's local file system. This - * is the video we want to generate an streamable HLS playlist for. - * - * @param outputPathPrefix The path to unique, unused and temporary prefix on - * the user's local file system. This function will write the generated HLS - * playlist and video segments under this prefix. - * - * @returns The paths to two files on the user's local file system - one - * containing the generated HLS playlist, and the other containing the - * transcoded and encrypted video segments that the HLS playlist refers to. - * - * If the video is such that it doesn't require stream generation, then this - * function returns `undefined`. - */ -export const ffmpegGenerateHLSPlaylistAndSegments = async ( - inputFilePath: string, - outputPathPrefix: string, -): Promise => { - const { isH264, isBT709, bitrate } = - await detectVideoCharacteristics(inputFilePath); - - log.debug(() => [basename(inputFilePath), { isH264, isBT709, bitrate }]); - - // If the video is smaller than 10 MB, and already H.264 (the codec we are - // going to use for the conversion), then a streaming variant is not much - // use. Skip such cases. - // - // --- - // - // [Note: HEVC/H.265 issues] - // - // We've observed two issues out in the wild with HEVC videos: - // - // 1. On Linux, HEVC video streams don't play. However, since the audio - // stream plays, the browser tells us that the "video" itself is - // playable, but the user sees a blank screen with only audio. - // - // 2. HEVC + HDR videos taken on an iPhone have a rotation (`Side data: - // displaymatrix` in the ffmpeg output) that Chrome (and thus Electron) - // doesn't take into account, so these play upside down. - // - // Not fully related to this case, but mentioning here as to why both the - // size and codec need to be checked before skipping stream generation. - if (isH264) { - const inputVideoSize = await fs - .stat(inputFilePath) - .then((st) => st.size); - if (inputVideoSize <= 10 * 1024 * 1024 /* 10 MB */) { - return undefined; - } - } - - // If the video is already H.264 with a bitrate less than 4000 kbps, then we - // do not need to reencode the video stream (by _far_ the costliest part of - // the HLS stream generation). - const reencodeVideo = !(isH264 && bitrate && bitrate <= 4000 * 1000); - - // If the bitrate is not too high, then we don't need to rescale the video - // when generating the video stream. This is not a performance optimization, - // but more for avoiding making the video size smaller unnecessarily. - const rescaleVideo = !(bitrate && bitrate <= 2000 * 1000); - - // [Note: Tonemapping HDR to HD] - // - // BT.709 ("HD") is a standard that describes things like how color is - // encoded, the range of values, and their "meaning" - i.e. how to map the - // values in the video to the pixels on the screen. - // - // It is not the only such standard, there are three common examples: - // - // - BT.601 ("Standard-Definition" or SD) - // - BT.709 ("High-Definition" or HD) - // - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^). - // - // ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for our - // purpose here we can treat it as as alias. - // - // BT.709 is the most common amongst these for older files out stored on - // computers, and they conform mostly to the standard (one notable exception - // is that the BT.709 standard also recommends using the yuv422p pixel - // format, but de facto yuv420p is used because many video players only - // support yuv420p). - // - // Since BT.709 is the most widely supported standard, we use it when - // generating the HLS playlist so to allow playback across the widest - // possible hardware/OS/browser combinations. - // - // If we convert HDR to HD without naively, then the colors look washed out - // compared to the original. To resolve this, we use a ffmpeg filterchain - // that uses the tonemap filter. - // - // However applying this tonemap to videos that are already HD leads to a - // brightness drop. So we conditionally apply this filter chain only if the - // colorspace is not already BT.709. - // - // See also: [Note: Alternative FFmpeg command for HDR videos], although - // that uses a allow-list based check (while here we use deny-list). - // - // Reference: - // - https://trac.ffmpeg.org/wiki/colorspace - const tonemap = !isBT709; - - // We want the generated playlist to refer to the chunks as "output.ts". - // - // So we arrange things accordingly: We use the `outputPathPrefix` as our - // working directory, and then ask ffmpeg to generate a playlist with the - // name "output.m3u8". - // - // ffmpeg will automatically place the segments in a file with the same base - // name as the playlist, but with a ".ts" extension. And since we use the - // "single_file" option, all the segments will be placed in a file named - // "output.ts". - - await fs.mkdir(outputPathPrefix); - - const playlistPath = path.join(outputPathPrefix, "output.m3u8"); - const videoPath = path.join(outputPathPrefix, "output.ts"); - - // Generate a cryptographically secure random key (16 bytes). - const keyBytes = randomBytes(16); - const keyB64 = keyBytes.toString("base64"); - - // Convert it to a data: URI that will be added to the playlist. - const keyURI = `data:text/plain;base64,${keyB64}`; - - // Determine two paths - one where we will write the key itself, and where - // we will write the "key info" that provides ffmpeg the `keyURI` and the - // `keyPath;. - const keyPath = playlistPath + ".key"; - const keyInfoPath = playlistPath + ".key-info"; - - // Generate a "key info": - // - // - the first line specifies the key URI that is written into the playlist. - // - the second line specifies the path to the local file system file from - // where ffmpeg should read the key. - const keyInfo = [keyURI, keyPath].join("\n"); - - // Overview: - // - // - Video H.264 HD 720p 30fps. - // - Audio AAC 128kbps. - // - Encrypted HLS playlist with a single file containing all the chunks. - // - // Reference: - // - `man ffmpeg-all` - // - https://trac.ffmpeg.org/wiki/Encode/H.264 - // - const command = [ - ffmpegBinaryPath(), - // Reduce the amount of output lines we have to parse. - ["-hide_banner"], - // Input file. We don't need any extra options that apply to the input file. - "-i", - inputFilePath, - // The remaining options apply to the next output file (`playlistPath`). - reencodeVideo - ? [ - // `-vf` creates a filter graph for the video stream. It is a - // comma separated list of filters chained together, e.g. - // `filter1=key=value:key=value.filter2=key=value`. - "-vf", - [ - // Do the rescaling to even number of pixels always if the - // tonemapping is going to be applied subsequently, - // otherwise the tonemapping will fail with "image - // dimensions must be divisible by subsampling factor". - // - // While we add the extra condition here for completeness, - // it won't usually matter since a non-BT.709 video is - // likely using a new codec, and as such would've a high - // enough bitrate to require rescaling anyways. - rescaleVideo || tonemap - ? [ - // Scales the video to maximum 720p height, - // keeping aspect ratio and the calculated - // dimension divisible by 2 (some of the other - // operations require an even pixel count). - "scale=-2:720", - // Convert the video to a constant 30 fps, - // duplicating or dropping frames as necessary. - "fps=30", - ] - : [], - // Convert the colorspace if the video is not in the HD - // color space (bt709). Before conversion, tone map colors - // so that they work the same across the change in the - // dyamic range. - // - // 1. The tonemap filter only works linear light, so we - // first use zscale with transfer=linear to linearize - // the input. - // - // 2. Then we use the tonemap, with the hable option that - // is best for preserving details. desat=0 turns off - // the default desaturation. - // - // 3. Use zscale again to "convert to BT.709" by asking it - // to set the all three of color primaries, transfer - // characteristics and colorspace matrix to 709 (Note: - // the constants specified in the tonemap filter help - // do not include the "bt" prefix) - // - // See: https://ffmpeg.org/ffmpeg-filters.html#tonemap-1 - // - // See: [Note: Tonemapping HDR to HD] - tonemap - ? [ - "zscale=transfer=linear", - "tonemap=tonemap=hable:desat=0", - "zscale=primaries=709:transfer=709:matrix=709", - ] - : [], - // Output using the well supported pixel format: 8-bit YUV - // planar color space with 4:2:0 chroma subsampling. - "format=yuv420p", - ] - .flat() - .join(","), - ] - : [], - reencodeVideo - ? // Video codec H.264 - // - // - `-c:v libx264` converts the video stream to the H.264 codec. - // - // - We don't supply a bitrate, instead it uses the default CRF - // ("23") as recommended in the ffmpeg trac. - // - // - We don't supply a preset, it'll use the default ("medium"). - ["-c:v", "libx264"] - : // Keep the video stream unchanged - ["-c:v", "copy"], - // Audio codec AAC - // - // - `-c:a aac` converts the audio stream to use the AAC codec - // - // - We don't supply a bitrate, it'll use the AAC default 128k bps. - ["-c:a", "aac"], - // Generate a HLS playlist. - ["-f", "hls"], - // Tell ffmpeg where to find the key, and the URI for the key to write - // into the generated playlist. Implies "-hls_enc 1". - ["-hls_key_info_file", keyInfoPath], - // Generate as many playlist entries as needed (default limit is 5). - ["-hls_list_size", "0"], - // Place all the video segments within the same .ts file (with the same - // path as the playlist file but with a ".ts" extension). - ["-hls_flags", "single_file"], - // Output path where the playlist should be generated. - playlistPath, - ].flat(); - - let dimensions: ReturnType; - let videoSize: number; - - try { - // Write the key and the keyInfo to their desired paths. - await Promise.all([ - fs.writeFile(keyPath, keyBytes), - fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }), - ]); - - // Run the ffmpeg command to generate the HLS playlist and segments. - // - // Note: Depending on the size of the input file, this may take long! - const { stderr: conversionStderr } = await execAsync(command); - - // Determine the dimensions of the generated video from the stderr - // output produced by ffmpeg during the conversion. - dimensions = detectVideoDimensions(conversionStderr); - - // Find the size of the generated video segments by reading the size of - // the generated .ts file. - videoSize = await fs.stat(videoPath).then((st) => st.size); - } catch (e) { - log.error("HLS generation failed", e); - await Promise.all([ - deleteTempFileIgnoringErrors(playlistPath), - deleteTempFileIgnoringErrors(videoPath), - ]); - throw e; - } finally { - await Promise.all([ - deleteTempFileIgnoringErrors(keyInfoPath), - deleteTempFileIgnoringErrors(keyPath), - // ffmpeg writes a /path/output.ts.tmp, clear it out too. - deleteTempFileIgnoringErrors(videoPath + ".tmp"), - ]); - } - - return { playlistPath, videoPath, dimensions, videoSize }; -}; - -/** - * A regex that matches the first line of the form - * - * Stream #0:0: Video: h264 (High 10) ([27][0][0][0] / 0x001B), yuv420p10le(tv, bt2020nc/bt2020/arib-std-b67), 1920x1080, 30 fps, 30 tbr, 90k tbn - * - * The part after Video: is the first capture group. - * - * Another example: - * - * Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default) - */ -const videoStreamLineRegex = /Stream #.+: Video:(.+)\n/; - -/** {@link videoStreamLineRegex}, but global. */ -const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g; - -/** - * A regex that matches " kb/s" preceded by a space. See - * {@link videoStreamLineRegex} for the context in which it is used. - */ -const videoBitrateRegex = / ([1-9]\d*) kb\/s/; - -/** - * A regex that matches x pair preceded by a space. See - * {@link videoStreamLineRegex} for the context in which it is used. - * - * We constrain the digit sequence not to begin with 0 to exclude hexadecimal - * representations of various constants that ffmpeg prints on this line (e.g. - * "avc1 / 0x31637661"). - */ -const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/; - -interface VideoCharacteristics { - isH264: boolean; - isBT709: boolean; - bitrate: number | undefined; -} -/** - * Heuristically determine information about the video at the given - * {@link inputFilePath}: - * - * - If is encoded using H.264 codec. - * - If it uses the BT.709 colorspace. - * - Its bitrate. - * - * The defaults are tailored for the cases in which these conditions are used, - * so that even if we get the detection wrong we'll only end up encoding videos - * that could've possibly been skipped as an optimization. - * - * [Note: Parsing CLI output might break on ffmpeg updates] - * - * This function tries to determine the these bits of information about the - * given video by scanning the ffmpeg info output for the video stream line, and - * doing various string matches and regex extractions. - * - * Needless to say, while this works currently, this is liable to break in the - * future. So if something stops working after updating ffmpeg, look here! - * - * Ideally, we'd have done this using `ffprobe`, but we don't have the ffprobe - * binary at hand, so we make do by grepping the log output of ffmpeg. - * - * For reference, - * - * - codec and colorspace are printed by the `avcodec_string` function in the - * ffmpeg source: - * https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.c - * - * - bitrate is printed by the `dump_stream_format` function in `dump.c`. - */ -const detectVideoCharacteristics = async (inputFilePath: string) => { - const videoInfo = await pseudoFFProbeVideo(inputFilePath); - const videoStreamLine = videoStreamLineRegex.exec(videoInfo)?.at(1)?.trim(); - - // Since the checks are heuristic, start with defaults that would cause the - // codec conversion to happen, even if it is unnecessary. - const res: VideoCharacteristics = { - isH264: false, - isBT709: false, - bitrate: undefined, - }; - if (!videoStreamLine) return res; - - res.isH264 = videoStreamLine.startsWith("h264 "); - res.isBT709 = videoStreamLine.includes("bt709"); - // The regex matches "\d kb/s", but there can be other units for the - // bitrate. However, (a) "kb/s" is the most common for videos out in the - // wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead - // of "-v:c copy", so only unnecessary processing but no change in output. - const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0); - if (brs) { - const br = parseInt(brs, 10); - if (br) res.bitrate = br; - } - - return res; -}; - -/** - * Heuristically detect the dimensions of the given video from the log output of - * the ffmpeg invocation during the HLS playlist generation. - * - * This function tries to determine the width and height of the generated video - * from the output log written by ffmpeg on its stderr during the generation - * process, scanning it for the last video stream line, and trying to match a - * "x" regex. - * - * See: [Note: Parsing CLI output might break on ffmpeg updates]. - */ -const detectVideoDimensions = (conversionStderr: string) => { - // There is a nicer way to do it - by running `pseudoFFProbeVideo` on the - // generated playlist. However, that playlist includes a data URL that - // specifies the encryption info, and ffmpeg refuses to read that unless we - // specify the "-allowed_extensions ALL" or something to that effect. - // - // Unfortunately, our current ffmpeg binary (5.x) does not support that - // option. So we instead parse the conversion output itself. - // - // This is also nice, since it saves on an extra ffmpeg invocation. But we - // now need to be careful to find the right video stream line, since the - // conversion output includes both the input and output video stream lines. - // - // To match the right (output) video stream line, we use a global regex, and - // use the last match since that'd correspond to the single video stream - // written in the output. - const videoStreamLine = Array.from( - conversionStderr.matchAll(videoStreamLinesRegex), - ) - .at(-1) /* Last Stream...: Video: line in the output */ - ?.at(1); /* First capture group */ - if (videoStreamLine) { - const [, ws, hs] = videoDimensionsRegex.exec(videoStreamLine) ?? []; - if (ws && hs) { - const w = parseInt(ws, 10); - const h = parseInt(hs, 10); - if (w && h) { - return { width: w, height: h }; - } - } - } - throw new Error( - `Unable to detect video dimensions from stream line [${videoStreamLine ?? ""}]`, +export const ffmpegDetermineVideoDuration = async ( + pathOrZipItem: string | ZipItem, +): Promise => + withInputFile(pathOrZipItem, async (worker, inputFilePath) => + worker.ffmpegDetermineVideoDuration(inputFilePath), ); -}; - -/** - * Heuristically detect if the file at given path is a HDR video. - * - * This is similar to {@link detectVideoCharacteristics}, and see that - * function's documentation for all the caveats. However, this function uses an - * allow-list instead, and considers any file with color transfer "smpte2084" or - * "arib-std-b67" to be HDR. While this is in some sense a more exact check, it - * comes with different caveats: - * - * - These particular constants are not guaranteed to be correct; these are just - * what I saw on the internet as being used / recommended for detecting HDR. - * - * - Since we don't have ffprobe, we're not checking the color space value - * itself but a substring of the stream line in the ffmpeg stderr output. - * - * In particular, we use this more exact check for places where we have less - * leeway. e.g. when generating thumbnails, if we apply the tonemapping to any - * non-BT.709 file (as the HLS stream generation does), we start getting the - * "code 3074: no path between colorspaces" error during the JPEG conversion - * (this is not a problem in the H.264 conversion). - * - * - See: [Note: Alternative FFmpeg command for HDR videos] - * - See: [Note: Tonemapping HDR to HD] - * - * @param inputFilePath The path to a video file on the user's machine. - * - * @returns `true` if this file is likely a HDR video. Exceptions are treated as - * `false` to make this function safe to invoke without breaking the happy path. - */ -const isHDRVideo = async (inputFilePath: string) => { - try { - const videoInfo = await pseudoFFProbeVideo(inputFilePath); - const vs = videoStreamLineRegex.exec(videoInfo)?.at(1); - if (!vs) return false; - return vs.includes("smpte2084") || vs.includes("arib-std-b67"); - } catch (e) { - log.warn(`Could not detect HDR status of ${inputFilePath}`, e); - return false; - } -}; - -/** - * Return the stderr of ffmpeg in an attempt to gain information about the video - * at the given {@link inputFilePath}. - * - * We don't have the ffprobe binary at hand, which is why we need to use this - * alternative. See: [Note: Parsing CLI output might break on ffmpeg updates] - * - * @returns the stderr of ffmpeg after running it on the input file. The exact - * command we run is: - * - * ffmpeg -i in.mov -an -frames:v 0 -f null - 2>info.txt - * - * And the returned string is the contents of the `info.txt` thus produced. - */ -const pseudoFFProbeVideo = async (inputFilePath: string) => { - const command = [ - ffmpegPathPlaceholder, - // Reduce the amount of output lines we have to parse. - ["-hide_banner"], - ["-i", inputPathPlaceholder], - "-an", - ["-frames:v", "0"], - ["-f", "null"], - "-", - ].flat(); - - const cmd = substitutePlaceholders(command, inputFilePath, /* NA */ ""); - - const { stderr } = await execAsync(cmd); - - return stderr; -}; diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 7daa101d2a..0ac99299eb 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -6,7 +6,7 @@ import { type ZipItem } from "../../types/ipc"; import { execAsync, isDev } from "../utils/electron"; import { deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "../utils/temp"; @@ -61,7 +61,7 @@ const vipsPath = () => ); export const generateImageThumbnail = async ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ): Promise => { @@ -69,7 +69,7 @@ export const generateImageThumbnail = async ( path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem); + } = await makeFileForStreamOrPathOrZipItem(pathOrZipItem); const outputFilePath = await makeTempFilePath("jpeg"); diff --git a/desktop/src/main/services/logout.ts b/desktop/src/main/services/logout.ts index 805eb6d109..14968ff24e 100644 --- a/desktop/src/main/services/logout.ts +++ b/desktop/src/main/services/logout.ts @@ -3,6 +3,7 @@ import log from "../log"; import { clearPendingVideoResults } from "../stream"; import { clearStores } from "./store"; import { watchReset } from "./watch"; +import { terminateUtilityProcesses } from "./workers"; import { clearOpenZipCache } from "./zip"; /** @@ -36,4 +37,9 @@ export const logout = (watcher: FSWatcher) => { } catch (e) { ignoreError("zip cache", e); } + try { + terminateUtilityProcesses(); + } catch (e) { + ignoreError("utility processes", e); + } }; diff --git a/desktop/src/main/services/ml-worker.ts b/desktop/src/main/services/ml-worker.ts index 5158925d31..35e342b029 100644 --- a/desktop/src/main/services/ml-worker.ts +++ b/desktop/src/main/services/ml-worker.ts @@ -15,47 +15,16 @@ import { existsSync } from "fs"; import fs from "node:fs/promises"; import path from "node:path"; import * as ort from "onnxruntime-node"; +import { z } from "zod/v4"; +import log from "../log-worker"; import { messagePortMainEndpoint } from "../utils/comlink"; import { wait } from "../utils/common"; import { writeStream } from "../utils/stream"; import { fsStatMtime } from "./fs"; -/** - * We cannot do - * - * import log from "../log"; - * - * because that requires the Electron APIs that are not available to a utility - * process (See: [Note: Using Electron APIs in UtilityProcess]). But even if - * that were to work, logging will still be problematic since we'd try opening - * the log file from two different Node.js processes (this one, and the main - * one), and I didn't find any indication in the electron-log repository that - * the log file's integrity would be maintained in such cases. - * - * So instead we create this proxy log object that uses `process.parentPort` to - * transport the logs over to the main process. - */ -const log = { - /** - * Unlike the real {@link log.error}, this accepts only the first string - * argument, not the second optional error one. - */ - errorString: (s: string) => mainProcess("log.errorString", s), - info: (...ms: unknown[]) => mainProcess("log.info", ms), - /** - * Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b) - * accepts only strings. - */ - debugString: (s: string) => mainProcess("log.debugString", s), -}; +log.debugString("Started ML utility process"); -/** - * Send a message to the main process using a barebones RPC protocol. - */ -const mainProcess = (method: string, param: unknown) => - process.parentPort.postMessage({ method, p: param }); - -log.debugString(`Started ML worker process`); +process.on("uncaughtException", (e, origin) => log.error(origin, e)); process.parentPort.once("message", (e) => { // Initialize ourselves with the data we got from our parent. @@ -84,17 +53,10 @@ let _userDataPath: string | undefined; /** Equivalent to app.getPath("userData") */ const userDataPath = () => _userDataPath!; +const MLWorkerInitData = z.object({ userDataPath: z.string() }); + const parseInitData = (data: unknown) => { - if ( - data && - typeof data == "object" && - "userDataPath" in data && - typeof data.userDataPath == "string" - ) { - _userDataPath = data.userDataPath; - } else { - log.errorString("Unparseable initialization data"); - } + _userDataPath = MLWorkerInitData.parse(data).userDataPath; }; /** @@ -161,7 +123,7 @@ const modelPathDownloadingIfNeeded = async ( } else { const size = (await fs.stat(modelPath)).size; if (size !== expectedByteSize) { - log.errorString( + log.error( `The size ${size} of model ${modelName} does not match the expected size, downloading again`, ); await downloadModel(modelPath, modelName); diff --git a/desktop/src/main/services/ml.ts b/desktop/src/main/services/ml.ts deleted file mode 100644 index d142ad5326..0000000000 --- a/desktop/src/main/services/ml.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * @file ML related functionality. This code runs in the main process. - */ - -import { - MessageChannelMain, - type BrowserWindow, - type UtilityProcess, -} from "electron"; -import { app, utilityProcess } from "electron/main"; -import path from "node:path"; -import log from "../log"; - -/** The active ML worker (utility) process, if any. */ -let _child: UtilityProcess | undefined; - -/** - * Create a new ML worker process, terminating the older ones (if any). - * - * [Note: ML IPC] - * - * The primary reason for doing ML tasks in the Node.js layer is so that we can - * use the binary ONNX runtime, which is 10-20x faster than the Wasm one that - * can be used directly on the web layer. - * - * For this to work, the main and renderer process need to communicate with each - * other. Further, in the web layer the ML indexing runs in a web worker (so as - * to not get in the way of the main thread). So the communication has 2 hops: - * - * Node.js main <-> Renderer main <-> Renderer web worker - * - * This naive way works, but has a problem. The Node.js main process is in the - * code path for delivering user events to the renderer process. The ML tasks we - * do take in the order of 100-300 ms (possibly more) for each individual - * inference. Thus, the Node.js main process is busy for those 100-300 ms, and - * does not forward events to the renderer, causing the UI to jitter. - * - * The solution for this is to spawn an Electron UtilityProcess, which we can - * think of a regular Node.js child process. This frees up the Node.js main - * process, and would remove the jitter. - * https://www.electronjs.org/docs/latest/tutorial/process-model - * - * It would seem that this introduces another hop in our IPC - * - * Node.js utility process <-> Node.js main <-> ... - * - * but here we can use the special bit about Electron utility processes that - * separates them from regular Node.js child processes: their support for - * message ports. https://www.electronjs.org/docs/latest/tutorial/message-ports - * - * As a brief summary, a MessagePort is a web feature that allows two contexts - * to communicate. A pair of message ports is called a message channel. The cool - * thing about these is that we can pass these ports themselves over IPC. - * - * > One caveat here is that the message ports can only be passed using the - * > `postMessage` APIs, not the usual send/invoke APIs. - * - * So we - * - * 1. In the utility process create a message channel. - * 2. Spawn a utility process, and send one port of the pair to it. - * 3. Send the other port of the pair to the renderer. - * - * The renderer will forward that port to the web worker that is coordinating - * the ML indexing on the web layer. Thereafter, the utility process and web - * worker can directly talk to each other! - * - * Node.js utility process <-> Renderer web worker - * - * The RPC protocol is handled using comlink on both ends. The port itself needs - * to be relayed using `postMessage`. - */ -export const createMLWorker = (window: BrowserWindow) => { - if (_child) { - log.debug(() => "Terminating previous ML worker process"); - _child.kill(); - _child = undefined; - } - - const { port1, port2 } = new MessageChannelMain(); - - const child = utilityProcess.fork(path.join(__dirname, "ml-worker.js")); - const userDataPath = app.getPath("userData"); - child.postMessage({ userDataPath }, [port1]); - - window.webContents.postMessage("createMLWorker/port", undefined, [port2]); - - handleMessagesFromUtilityProcess(child); - - _child = child; -}; - -/** - * Handle messages posted from the utility process. - * - * [Note: Using Electron APIs in UtilityProcess] - * - * Only a small subset of the Electron APIs are available to a UtilityProcess. - * As of writing (Jul 2024, Electron 30), only the following are available: - * - * - net - * - systemPreferences - * - * In particular, `app` is not available. - * - * We structure our code so that it doesn't need anything apart from `net`. - * - * For the other cases, - * - * - Additional parameters to the utility process are passed alongwith the - * initial message where we provide it the message port. - * - * - When we need to communicate from the utility process to the main process, - * we use the `parentPort` in the utility process. - */ -const handleMessagesFromUtilityProcess = (child: UtilityProcess) => { - const logTag = "[ml-worker]"; - child.on("message", (m: unknown) => { - if (m && typeof m == "object" && "method" in m && "p" in m) { - const p = m.p; - switch (m.method) { - case "log.errorString": - if (typeof p == "string") { - log.error(`${logTag} ${p}`); - return; - } - break; - case "log.info": - if (Array.isArray(p)) { - // Need to cast from any[] to unknown[] - log.info(logTag, ...(p as unknown[])); - return; - } - break; - case "log.debugString": - if (typeof p == "string") { - log.debug(() => `${logTag} ${p}`); - return; - } - break; - default: - break; - } - } - log.info("Ignoring unknown message from ML worker", m); - }); -}; diff --git a/desktop/src/main/services/store.ts b/desktop/src/main/services/store.ts index f4f5a9369a..227fe691c6 100644 --- a/desktop/src/main/services/store.ts +++ b/desktop/src/main/services/store.ts @@ -24,17 +24,17 @@ export const clearStores = () => { * On macOS, `safeStorage` stores our data under a Keychain entry named * " Safe Storage". In our case, "ente Safe Storage". */ -export const saveMasterKeyB64 = (masterKeyB64: string) => { - const encryptedKey = safeStorage.encryptString(masterKeyB64); - const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); - safeStorageStore.set("encryptionKey", b64EncryptedKey); +export const saveMasterKeyInSafeStorage = (masterKey: string) => { + const encryptedKeyBuffer = safeStorage.encryptString(masterKey); + const encryptedKey = Buffer.from(encryptedKeyBuffer).toString("base64"); + safeStorageStore.set("encryptionKey", encryptedKey); }; -export const masterKeyB64 = (): string | undefined => { - const b64EncryptedKey = safeStorageStore.get("encryptionKey"); - if (!b64EncryptedKey) return undefined; - const keyBuffer = Buffer.from(b64EncryptedKey, "base64"); - return safeStorage.decryptString(keyBuffer); +export const masterKeyFromSafeStorage = (): string | undefined => { + const encryptedKey = safeStorageStore.get("encryptionKey"); + if (!encryptedKey) return undefined; + const encryptedKeyBuffer = Buffer.from(encryptedKey, "base64"); + return safeStorage.decryptString(encryptedKeyBuffer); }; export const lastShownChangelogVersion = (): number | undefined => diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 6f198c513b..4066a91c89 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -28,6 +28,13 @@ export const createWatcher = (mainWindow: BrowserWindow) => { // Ask the watcher to wait for a the file size to stabilize before // telling us about a new file. By default, it waits for 2 seconds. awaitWriteFinish: true, + // On macOS we start getting "EMFILE: too many open files" when watching + // large folders. This is a known regression in Chokidar v4: + // https://github.com/paulmillr/chokidar/issues/1385 + // + // The recommended workaround for now is to enable usePolling. Since it + // comes at a performance cost, we only do it where needed (macOS). + ...(process.platform == "darwin" ? { usePolling: true } : {}), }); watcher diff --git a/desktop/src/main/services/workers.ts b/desktop/src/main/services/workers.ts new file mode 100644 index 0000000000..916292ceb6 --- /dev/null +++ b/desktop/src/main/services/workers.ts @@ -0,0 +1,241 @@ +/** + * @file This main process code and interface for dealing with the various + * utility processes that we create. + */ + +import type { Endpoint } from "comlink"; +import { + MessageChannelMain, + type BrowserWindow, + type UtilityProcess, +} from "electron"; +import { app, utilityProcess } from "electron/main"; +import path from "node:path"; +import type { UtilityProcessType } from "../../types/ipc"; +import log, { processUtilityProcessLogMessage } from "../log"; +import { messagePortMainEndpoint } from "../utils/comlink"; + +/** + * Terminate any existing utility processes if they're running. + * + * This function is called during the logout sequence. + */ +export const terminateUtilityProcesses = () => { + terminateMLProcessIfRunning(); + terminateFFmpegProcessIfRunning(); +}; + +/** The active ML utility process, if any. */ +let _utilityProcessML: UtilityProcess | undefined; + +/** The active FFmpeg utility process, if any. */ +let _utilityProcessFFmpeg: UtilityProcess | undefined; + +/** + * A promise to a comlink {@link Endpoint} that can be used to communicate with + * the active ffmpeg utility process (if any). + */ +let _utilityProcessFFmpegEndpoint: Promise | undefined; + +/** + * Create a new utility process of the given {@link type}, terminating the older + * ones (if any). + * + * Currently the only type is "ml". The following note explains the reasoning + * why utility processes were used for the first workload (ML) that was handled + * this way. Similar reasoning applies to subsequent workloads (ffmpeg) that + * have been offloaded to utility processes in a slightly different manner to + * avoid stutter in the UI. + * + * [Note: ML IPC] + * + * The primary reason for doing ML tasks in the Node.js layer is so that we can + * use the binary ONNX runtime, which is 10-20x faster than the Wasm one that + * can be used directly on the web layer. + * + * For this to work, the main and renderer process need to communicate with each + * other. Further, in the web layer the ML indexing runs in a web worker (so as + * to not get in the way of the main thread). So the communication has 2 hops: + * + * Node.js main <-> Renderer main <-> Renderer web worker + * + * This naive way works, but has a problem. The Node.js main process is in the + * code path for delivering user events to the renderer process. The ML tasks we + * do take in the order of 100-300 ms (possibly more) for each individual + * inference. Thus, the Node.js main process is busy for those 100-300 ms, and + * does not forward events to the renderer, causing the UI to jitter. + * + * The solution for this is to spawn an Electron UtilityProcess, which we can + * think of a regular Node.js child process. This frees up the Node.js main + * process, and would remove the jitter. + * https://www.electronjs.org/docs/latest/tutorial/process-model + * + * It would seem that this introduces another hop in our IPC + * + * Node.js utility process <-> Node.js main <-> ... + * + * but here we can use the special bit about Electron utility processes that + * separates them from regular Node.js child processes: their support for + * message ports. https://www.electronjs.org/docs/latest/tutorial/message-ports + * + * As a brief summary, a MessagePort is a web feature that allows two contexts + * to communicate. A pair of message ports is called a message channel. The cool + * thing about these is that we can pass these ports themselves over IPC. + * + * > One caveat here is that the message ports can only be passed using the + * > `postMessage` APIs, not the usual send/invoke APIs. + * + * So we + * + * 1. In the utility process create a message channel. + * 2. Spawn a utility process, and send one port of the pair to it. + * 3. Send the other port of the pair to the renderer. + * + * The renderer will forward that port to the web worker that is coordinating + * the ML indexing on the web layer. Thereafter, the utility process and web + * worker can directly talk to each other! + * + * Node.js utility process <-> Renderer web worker + * + * The RPC protocol is handled using comlink on both ends. The port itself needs + * to be relayed using `postMessage`. + */ +export const triggerCreateUtilityProcess = ( + type: UtilityProcessType, + window: BrowserWindow, +) => triggerCreateMLUtilityProcess(window); + +const terminateMLProcessIfRunning = () => { + if (_utilityProcessML) { + log.debug(() => "Terminating running ML utility process"); + _utilityProcessML.kill(); + _utilityProcessML = undefined; + } +}; + +export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => { + terminateMLProcessIfRunning(); + + const { port1, port2 } = new MessageChannelMain(); + + const child = utilityProcess.fork(path.join(__dirname, "ml-worker.js")); + const userDataPath = app.getPath("userData"); + child.postMessage(/* MLWorkerInitData */ { userDataPath }, [port1]); + + window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]); + + handleMessagesFromMLUtilityProcess(child); + + _utilityProcessML = child; +}; + +/** + * Handle messages posted from the utility process. + * + * [Note: Using Electron APIs in UtilityProcess] + * + * Only a small subset of the Electron APIs are available to a UtilityProcess. + * As of writing (Jul 2024, Electron 30), only the following are available: + * + * - net + * - systemPreferences + * + * In particular, `app` is not available. + * + * We structure our code so that it doesn't need anything apart from `net`. + * + * For the other cases, + * + * - Additional parameters to the utility process are passed alongwith the + * initial message where we provide it the message port. + * + * - When we need to communicate from the utility process to the main process, + * we use the `parentPort` in the utility process. + */ +const handleMessagesFromMLUtilityProcess = (child: UtilityProcess) => { + child.on("message", (m: unknown) => { + if (processUtilityProcessLogMessage("[ml-worker]", m)) { + return; + } + log.info("Ignoring unknown message from ML utility process", m); + }); +}; + +/** + * A comlink endpoint that can be used to communicate with the ffmpeg utility + * process. If there is no ffmpeg utility process, a new one is created on + * demand. + * + * See [Note: ML IPC] for a general outline of why utility processes are needed + * (tl;dr; to avoid stutter on the UI). + * + * In the case of ffmpeg, the IPC flow is a bit different: the utility process + * is not exposed to the web layer, and is internal to the node layer. The + * reason for this difference is that we need to create temporary files etc, and + * doing it a utility process requires access to the `app` module which are not + * accessible (See: [Note: Using Electron APIs in UtilityProcess]). + * + * There could've been possible reasonable workarounds, but the architecture + * we've adopted of three layers: + * + * Renderer (web) <-> Node.js main <-> Node.js ffmpeg utility process + * + * The temporary file creation etc is handled in the Node.js main process, and + * paths to the files are forwarded to the ffmpeg utility process to act on. + * + * @returns an endpoint that can be used to communicate with the utility + * process. The utility process is expected to expose an object that conforms to + * the {@link ElectronFFmpegWorkerNode} interface on this endpoint. + */ +export const ffmpegUtilityProcessEndpoint = () => + (_utilityProcessFFmpegEndpoint ??= createFFmpegUtilityProcessEndpoint()); + +const terminateFFmpegProcessIfRunning = () => { + if (_utilityProcessFFmpeg) { + log.debug(() => "Terminating running FFmpeg utility process"); + _utilityProcessFFmpeg.kill(); + _utilityProcessFFmpeg = undefined; + _utilityProcessFFmpegEndpoint = undefined; + } +}; + +const createFFmpegUtilityProcessEndpoint = () => { + if (_utilityProcessFFmpeg) { + throw new Error("FFmpeg utility process is already running"); + } + + // Promise.withResolvers is currently in the node available to us. + let resolve: ((endpoint: Endpoint) => void) | undefined; + const promise = new Promise((r) => (resolve = r)); + + const { port1, port2 } = new MessageChannelMain(); + + const child = utilityProcess.fork(path.join(__dirname, "ffmpeg-worker.js")); + // Send a handle to the port (one end of the message channel) to the utility + // process (alongwith any other init data). The utility process will reply + // with an "ack" when it get it. + const appVersion = app.getVersion(); + child.postMessage(/* FFmpegWorkerInitData */ { appVersion }, [port1]); + + child.on("message", (m: unknown) => { + if (m && typeof m == "object" && "method" in m) { + switch (m.method) { + case "ack": + resolve!(messagePortMainEndpoint(port2)); + return; + } + } + + if (processUtilityProcessLogMessage("[ffmpeg-worker]", m)) { + return; + } + + log.info("Ignoring unknown message from ffmpeg utility process", m); + }); + + _utilityProcessFFmpeg = child; + + // Resolve with the other end of the message channel (once we get an "ack" + // from the utility process). + return promise; +}; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 7007debe1d..35339f9a9e 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -3,23 +3,18 @@ */ import { net, protocol } from "electron/main"; import { randomUUID } from "node:crypto"; -import fs_ from "node:fs"; import fs from "node:fs/promises"; -import { Readable, Writable } from "node:stream"; +import { Writable } from "node:stream"; import { pathToFileURL } from "node:url"; import log from "./log"; -import { - ffmpegConvertToMP4, - ffmpegGenerateHLSPlaylistAndSegments, - type FFmpegGenerateHLSPlaylistAndSegmentsResult, -} from "./services/ffmpeg"; +import { ffmpegUtilityProcess } from "./services/ffmpeg"; +import { type FFmpegGenerateHLSPlaylistAndSegmentsResult } from "./services/ffmpeg-worker"; import { markClosableZip, openZip } from "./services/zip"; -import { wait } from "./utils/common"; import { writeStream } from "./utils/stream"; import { deleteTempFile, deleteTempFileIgnoringErrors, - makeFileForDataOrStreamOrPathOrZipItem, + makeFileForStreamOrPathOrZipItem, makeTempFilePath, } from "./utils/temp"; @@ -234,12 +229,14 @@ export const clearPendingVideoResults = () => pendingVideoResults.clear(); * See also: [Note: IPC streams] */ const handleConvertToMP4Write = async (request: Request) => { + const worker = await ffmpegUtilityProcess(); + const inputTempFilePath = await makeTempFilePath(); await writeStream(inputTempFilePath, request.body!); const outputTempFilePath = await makeTempFilePath("mp4"); try { - await ffmpegConvertToMP4(inputTempFilePath, outputTempFilePath); + await worker.ffmpegConvertToMP4(inputTempFilePath, outputTempFilePath); } catch (e) { log.error("Conversion to MP4 failed", e); await deleteTempFileIgnoringErrors(outputTempFilePath); @@ -280,9 +277,9 @@ const handleVideoDone = async (token: string) => { * * The difference here is that we the conversion generates two streams^ - one * for the HLS playlist itself, and one for the file containing the encrypted - * and transcoded video chunks. The video stream we write to the objectUploadURL - * (provided via {@link params}), and then we return a JSON object containing - * the token for the playlist, and other metadata for use by the renderer. + * and transcoded video chunks. The video stream we write to the pre-signed + * object upload URL(s), and then we return a JSON object containing the token + * for the playlist, and other metadata for use by the renderer. * * ^ if the video doesn't require a stream to be generated (e.g. it is very * small and already uses a compatible codec) then a HTT 204 is returned and @@ -292,10 +289,12 @@ const handleGenerateHLSWrite = async ( request: Request, params: URLSearchParams, ) => { - const objectUploadURL = params.get("objectUploadURL"); - if (!objectUploadURL) throw new Error("Missing objectUploadURL"); + const fileID = parseInt(params.get("fileID") ?? "", 10); + const fetchURL = params.get("fetchURL"); + const authToken = params.get("authToken"); + if (!fileID || !fetchURL || !authToken) throw new Error("Missing params"); - let inputItem: Parameters[0]; + let inputItem: Parameters[0]; const path = params.get("path"); if (path) { inputItem = path; @@ -311,20 +310,25 @@ const handleGenerateHLSWrite = async ( } } + const worker = await ffmpegUtilityProcess(); + const { path: inputFilePath, isFileTemporary: isInputFileTemporary, writeToTemporaryFile: writeToTemporaryInputFile, - } = await makeFileForDataOrStreamOrPathOrZipItem(inputItem); + } = await makeFileForStreamOrPathOrZipItem(inputItem); const outputFilePathPrefix = await makeTempFilePath(); let result: FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined; try { await writeToTemporaryInputFile(); - result = await ffmpegGenerateHLSPlaylistAndSegments( + result = await worker.ffmpegGenerateHLSPlaylistAndSegments( inputFilePath, outputFilePathPrefix, + fileID, + fetchURL, + authToken, ); if (!result) { @@ -332,94 +336,22 @@ const handleGenerateHLSWrite = async ( return new Response(null, { status: 204 }); } - const { playlistPath, videoPath } = result; - try { - await uploadVideoSegments(videoPath, objectUploadURL); + const { playlistPath, dimensions, videoSize, videoObjectID } = result; - const playlistToken = randomUUID(); - pendingVideoResults.set(playlistToken, playlistPath); + const playlistToken = randomUUID(); + pendingVideoResults.set(playlistToken, playlistPath); - const { dimensions, videoSize } = result; - return new Response( - JSON.stringify({ playlistToken, dimensions, videoSize }), - { status: 200 }, - ); - } catch (e) { - await deleteTempFileIgnoringErrors(playlistPath); - throw e; - } finally { - await deleteTempFileIgnoringErrors(videoPath); - } + return new Response( + JSON.stringify({ + playlistToken, + dimensions, + videoSize, + videoObjectID, + }), + { status: 200 }, + ); } finally { if (isInputFileTemporary) await deleteTempFileIgnoringErrors(inputFilePath); } }; - -/** - * Upload the file at the given {@link videoFilePath} to the provided presigned - * {@link objectUploadURL} using a HTTP PUT request. - * - * In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff. - * - * See: [Note: Upload HLS video segment from node side]. - * - * --- - * - * This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx` - * from `web/packages/base/http.ts` (we don't have the rest of the scaffolding - * used by that function, which is why it is inlined bespoked). - * - * It handles the specific use case of uploading videos since generating the HLS - * stream is a fairly expensive operation, so a retry to discount transient - * network issues is called for. There are only 2 retries for a total of 3 - * attempts, and the retry gaps are more spaced out. - */ -export const uploadVideoSegments = async ( - videoFilePath: string, - objectUploadURL: string, -) => { - const waitTimeBeforeNextTry = [5000, 20000]; - - while (true) { - let abort = false; - try { - const nodeStream = fs_.createReadStream(videoFilePath); - const webStream = Readable.toWeb(nodeStream); - - const res = await net.fetch(objectUploadURL, { - method: "PUT", - // The duplex option is required since we're passing a stream. - // - // @ts-expect-error TypeScript's libdom.d.ts does not include - // the "duplex" parameter, e.g. see - // https://github.com/node-fetch/node-fetch/issues/1769. - duplex: "half", - body: webStream, - }); - - if (res.ok) { - // Success. - return; - } - if (res.status >= 400 && res.status < 500) { - // HTTP 4xx. - abort = true; - } - throw new Error( - `Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`, - ); - } catch (e) { - if (abort) { - throw e; - } - const t = waitTimeBeforeNextTry.shift(); - if (!t) { - throw e; - } else { - log.warn("Will retry potentially transient request failure", e); - } - await wait(t); - } - } -}; diff --git a/desktop/src/main/utils/comlink.ts b/desktop/src/main/utils/comlink.ts index d2006e795b..f0edd758af 100644 --- a/desktop/src/main/utils/comlink.ts +++ b/desktop/src/main/utils/comlink.ts @@ -19,7 +19,7 @@ export const messagePortMainEndpoint = (mp: MessagePortMain): Endpoint => { const listeners = new WeakMap(); return { postMessage: (message, transfer) => { - mp.postMessage(message, transfer as unknown as MessagePortMain[]); + mp.postMessage(message, (transfer ?? []) as MessagePortMain[]); }, addEventListener: (_, eh) => { const l: EL = (data) => diff --git a/desktop/src/main/utils/common.ts b/desktop/src/main/utils/common.ts index b123014270..7086e63407 100644 --- a/desktop/src/main/utils/common.ts +++ b/desktop/src/main/utils/common.ts @@ -15,3 +15,11 @@ */ export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Convert `null` to `undefined`, passthrough everything else unchanged. + * + * Duplicated from `web/packages/utils/transform.ts`. + */ +export const nullToUndefined = (v: T | null | undefined): T | undefined => + v === null ? undefined : v; diff --git a/desktop/src/main/utils/exec-worker.ts b/desktop/src/main/utils/exec-worker.ts new file mode 100644 index 0000000000..02ac116a6d --- /dev/null +++ b/desktop/src/main/utils/exec-worker.ts @@ -0,0 +1,23 @@ +import shellescape from "any-shell-escape"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import log from "../log-worker"; + +/** + * Run a shell command asynchronously (utility process edition). + * + * This is an almost verbatim copy of {@link execAsync} from `electron.ts`, + * except it is meant to be usable from a utility process where only a subset of + * imports are available. See [Note: Using Electron APIs in UtilityProcess]. + */ +export const execAsyncWorker = async (command: string | string[]) => { + const escapedCommand = Array.isArray(command) + ? shellescape(command) + : command; + const startTime = Date.now(); + const result = await execAsync_(escapedCommand); + log.debugString(`${escapedCommand} (${Date.now() - startTime} ms)`); + return result; +}; + +const execAsync_ = promisify(exec); diff --git a/desktop/src/main/utils/http.ts b/desktop/src/main/utils/http.ts new file mode 100644 index 0000000000..91a476fed9 --- /dev/null +++ b/desktop/src/main/utils/http.ts @@ -0,0 +1,31 @@ +export const clientPackageName = "io.ente.photos.desktop"; + +/** + * Reimplementation of {@link publicRequestHeaders} from the web source. + * + * @param desktopAppVersion The desktop app's version. This will get passed on + * as the "X-Client-Version" header. + * + * We cannot directly use `app.getVersion()` to obtain this value since the + * {@link app} module is not accessible to Electron utility processes which also + * calls this function. + */ +export const publicRequestHeaders = (desktopAppVersion: string) => ({ + "X-Client-Package": clientPackageName, + "X-Client-Version": desktopAppVersion, +}); + +/** + * Reimplementation of {@link authenticatedRequestHeaders} from the web source. + * + * This builds on top of {@link publicRequestHeaders} and takes the same + * parameters, and additionally also requires the {@link authToken} that will be + * passed as the "X-Auth-Token" header. + */ +export const authenticatedRequestHeaders = ( + desktopAppVersion: string, + authToken: string, +) => ({ + ...publicRequestHeaders(desktopAppVersion), + "X-Auth-Token": authToken, +}); diff --git a/desktop/src/main/utils/temp.ts b/desktop/src/main/utils/temp.ts index 67fd9ae1b9..ad874dd216 100644 --- a/desktop/src/main/utils/temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -80,8 +80,8 @@ export const deleteTempFileIgnoringErrors = async (tempFilePath: string) => { } }; -/** The result of {@link makeFileForDataOrStreamOrPathOrZipItem}. */ -interface FileForDataOrPathOrZipItem { +/** The result of {@link makeFileForStreamOrPathOrZipItem}. */ +interface FileForStreamOrPathOrZipItem { /** * The path to the file (possibly temporary). */ @@ -107,13 +107,13 @@ interface FileForDataOrPathOrZipItem { * that needs to be deleted after processing, and a function to write the given * {@link item} into that temporary file if needed. * - * @param item The contents of the file (bytes), or a {@link ReadableStream} - * with the contents of the file, or the path to an existing file, or a (path to - * a zip file, name of an entry within that zip file) tuple. + * @param item A {@link ReadableStream} with the contents of the file, or the + * path to an existing file, or a (path to a zip file, name of an entry within + * that zip file) tuple. */ -export const makeFileForDataOrStreamOrPathOrZipItem = async ( - item: Uint8Array | ReadableStream | string | ZipItem, -): Promise => { +export const makeFileForStreamOrPathOrZipItem = async ( + item: ReadableStream | string | ZipItem, +): Promise => { let path: string; let isFileTemporary: boolean; let writeToTemporaryFile = async () => { @@ -126,9 +126,7 @@ export const makeFileForDataOrStreamOrPathOrZipItem = async ( } else { path = await makeTempFilePath(); isFileTemporary = true; - if (item instanceof Uint8Array) { - writeToTemporaryFile = () => fs.writeFile(path, item); - } else if (item instanceof ReadableStream) { + if (item instanceof ReadableStream) { writeToTemporaryFile = () => writeStream(path, item); } else { writeToTemporaryFile = async () => { diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index ef586210dd..ea6a7af7b2 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -69,6 +69,7 @@ import type { FFmpegCommand, FolderWatch, PendingUploads, + UtilityProcessType, ZipItem, } from "./types/ipc"; @@ -112,10 +113,11 @@ const logout = () => { return ipcRenderer.invoke("logout"); }; -const masterKeyB64 = () => ipcRenderer.invoke("masterKeyB64"); +const masterKeyFromSafeStorage = () => + ipcRenderer.invoke("masterKeyFromSafeStorage"); -const saveMasterKeyB64 = (masterKeyB64: string) => - ipcRenderer.invoke("saveMasterKeyB64", masterKeyB64); +const saveMasterKeyInSafeStorage = (masterKey: string) => + ipcRenderer.invoke("saveMasterKeyInSafeStorage", masterKey); const lastShownChangelogVersion = () => ipcRenderer.invoke("lastShownChangelogVersion"); @@ -192,41 +194,45 @@ const convertToJPEG = (imageData: Uint8Array) => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ) => ipcRenderer.invoke( "generateImageThumbnail", - dataOrPathOrZipItem, + pathOrZipItem, maxDimension, maxSize, ); const ffmpegExec = ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, ) => ipcRenderer.invoke( "ffmpegExec", command, - dataOrPathOrZipItem, + pathOrZipItem, outputFileExtension, ); -// - ML +const ffmpegDetermineVideoDuration = (pathOrZipItem: string | ZipItem) => + ipcRenderer.invoke("ffmpegDetermineVideoDuration", pathOrZipItem); -const createMLWorker = () => { +// - Utility processes + +const triggerCreateUtilityProcess = (type: UtilityProcessType) => { + const portEvent = `utilityProcessPort/${type}`; const l = (event: IpcRendererEvent) => { void windowLoaded.then(() => { // "*"" is the origin to send to. - window.postMessage("createMLWorker/port", "*", event.ports); - ipcRenderer.off("createMLWorker/port", l); + window.postMessage(portEvent, "*", event.ports); + ipcRenderer.off(portEvent, l); }); }; - ipcRenderer.on("createMLWorker/port", l); - ipcRenderer.send("createMLWorker"); + ipcRenderer.on(portEvent, l); + ipcRenderer.send("triggerCreateUtilityProcess", type); }; // - Watch @@ -353,8 +359,8 @@ contextBridge.exposeInMainWorld("electron", { selectDirectory, pathForFile, logout, - masterKeyB64, - saveMasterKeyB64, + masterKeyFromSafeStorage, + saveMasterKeyInSafeStorage, lastShownChangelogVersion, setLastShownChangelogVersion, isAutoLaunchEnabled, @@ -390,10 +396,11 @@ contextBridge.exposeInMainWorld("electron", { convertToJPEG, generateImageThumbnail, ffmpegExec, + ffmpegDetermineVideoDuration, // - ML - createMLWorker, + triggerCreateUtilityProcess, // - Watch diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 7701836217..db1d009f8b 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -5,6 +5,8 @@ * See [Note: types.ts <-> preload.ts <-> ipc.ts] */ +export type UtilityProcessType = "ml"; + export interface AppUpdate { autoUpdatable: boolean; version: string; diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 25b3a8f43a..4c00433d43 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -25,7 +25,7 @@ ajv "^6.12.0" ajv-keywords "^3.4.1" -"@electron/asar@3.4.1": +"@electron/asar@3.4.1", "@electron/asar@^3.3.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065" integrity sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA== @@ -34,15 +34,6 @@ glob "^7.1.6" minimatch "^3.0.4" -"@electron/asar@^3.2.7": - version "3.2.18" - resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517" - integrity sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg== - dependencies: - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - "@electron/fuses@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@electron/fuses/-/fuses-1.8.0.tgz#ad34d3cc4703b1258b83f6989917052cfc1490a0" @@ -91,10 +82,10 @@ fs-extra "^9.0.1" promise-retry "^2.0.1" -"@electron/osx-sign@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.3.1.tgz#faf7eeca7ca004a6be541dc4cf7a1bd59ec59b1c" - integrity sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw== +"@electron/osx-sign@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.3.3.tgz#af751510488318d9f7663694af85819690d75583" + integrity sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg== dependencies: compare-version "^0.1.2" debug "^4.3.4" @@ -123,12 +114,12 @@ tar "^6.0.5" yargs "^17.0.1" -"@electron/universal@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-2.0.1.tgz#7b070ab355e02957388f3dbd68e2c3cd08c448ae" - integrity sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA== +"@electron/universal@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-2.0.3.tgz#1680df6ced8f128ca0ff24e29c2165d41d78b3ce" + integrity sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g== dependencies: - "@electron/asar" "^3.2.7" + "@electron/asar" "^3.3.1" "@malept/cross-spawn-promise" "^2.0.0" debug "^4.3.1" dir-compare "^4.2.0" @@ -136,13 +127,20 @@ minimatch "^9.0.3" plist "^3.1.0" -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0": version "4.11.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" @@ -177,10 +175,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06" integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ== -"@eslint/js@^9.25.1": - version "9.25.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117" - integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg== +"@eslint/js@^9.28.0": + version "9.28.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.28.0.tgz#7822ccc2f8cae7c3cd4f902377d520e9ae03f844" + integrity sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg== "@eslint/object-schema@^2.1.4": version "2.1.4" @@ -202,6 +200,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/fs-minipass@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" @@ -268,10 +278,10 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" - integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@pkgr/core@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== "@sindresorhus/is@^4.0.0": version "4.6.0" @@ -290,10 +300,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@tsconfig/node22@^22.0.1": - version "22.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.1.tgz#27e3ee9b359e31e5b94690bf2bad5a923c1d57d0" - integrity sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg== +"@tsconfig/node22@^22.0.2": + version "22.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.2.tgz#1e04e2c5cc946dac787d69bb502462a851ae51b6" + integrity sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA== "@types/auto-launch@^5.0.5": version "5.0.5" @@ -385,85 +395,101 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a" - integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ== +"@typescript-eslint/eslint-plugin@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz#532641b416ed2afd5be893cddb2a58e9cd1f7a3e" + integrity sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/type-utils" "8.31.1" - "@typescript-eslint/utils" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.33.1" + "@typescript-eslint/type-utils" "8.33.1" + "@typescript-eslint/utils" "8.33.1" + "@typescript-eslint/visitor-keys" "8.33.1" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^7.0.0" natural-compare "^1.4.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b" - integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q== +"@typescript-eslint/parser@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.33.1.tgz#ef9a5ee6aa37a6b4f46cc36d08a14f828238afe2" + integrity sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA== dependencies: - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/scope-manager" "8.33.1" + "@typescript-eslint/types" "8.33.1" + "@typescript-eslint/typescript-estree" "8.33.1" + "@typescript-eslint/visitor-keys" "8.33.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b" - integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw== +"@typescript-eslint/project-service@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.33.1.tgz#c85e7d9a44d6a11fe64e73ac1ed47de55dc2bf9f" + integrity sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" - -"@typescript-eslint/type-utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c" - integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA== - dependencies: - "@typescript-eslint/typescript-estree" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/tsconfig-utils" "^8.33.1" + "@typescript-eslint/types" "^8.33.1" debug "^4.3.4" - ts-api-utils "^2.0.1" -"@typescript-eslint/types@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4" - integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ== - -"@typescript-eslint/typescript-estree@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf" - integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag== +"@typescript-eslint/scope-manager@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz#d1e0efb296da5097d054bc9972e69878a2afea73" + integrity sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA== dependencies: - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/visitor-keys" "8.31.1" + "@typescript-eslint/types" "8.33.1" + "@typescript-eslint/visitor-keys" "8.33.1" + +"@typescript-eslint/tsconfig-utils@8.33.1", "@typescript-eslint/tsconfig-utils@^8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz#7836afcc097a4657a5ed56670851a450d8b70ab8" + integrity sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g== + +"@typescript-eslint/type-utils@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz#d73ee1a29d8a0abe60d4abbff4f1d040f0de15fa" + integrity sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww== + dependencies: + "@typescript-eslint/typescript-estree" "8.33.1" + "@typescript-eslint/utils" "8.33.1" + debug "^4.3.4" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@8.33.1", "@typescript-eslint/types@^8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.33.1.tgz#b693111bc2180f8098b68e9958cf63761657a55f" + integrity sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg== + +"@typescript-eslint/typescript-estree@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz#d271beed470bc915b8764e22365d4925c2ea265d" + integrity sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA== + dependencies: + "@typescript-eslint/project-service" "8.33.1" + "@typescript-eslint/tsconfig-utils" "8.33.1" + "@typescript-eslint/types" "8.33.1" + "@typescript-eslint/visitor-keys" "8.33.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14" - integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ== +"@typescript-eslint/utils@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.33.1.tgz#ea22f40d3553da090f928cf17907e963643d4b96" + integrity sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.31.1" - "@typescript-eslint/types" "8.31.1" - "@typescript-eslint/typescript-estree" "8.31.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.33.1" + "@typescript-eslint/types" "8.33.1" + "@typescript-eslint/typescript-estree" "8.33.1" -"@typescript-eslint/visitor-keys@8.31.1": - version "8.31.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75" - integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw== +"@typescript-eslint/visitor-keys@8.33.1": + version "8.33.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz#6c6e002c24d13211df3df851767f24dfdb4f42bc" + integrity sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ== dependencies: - "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/types" "8.33.1" eslint-visitor-keys "^4.2.0" "@xmldom/xmldom@^0.8.8": @@ -569,44 +595,45 @@ app-builder-bin@5.0.0-alpha.12: resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80" integrity sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w== -app-builder-lib@26.0.14: - version "26.0.14" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.14.tgz#a28fbefb600cf052d1259932f32289e043573f61" - integrity sha512-nc/A9MUd95MCc7bR4yVW7Lhs9FZTA/l8QdV8PE1vZZOOiogK4dupBfCCJG0UqLU81JS62f078/bwAeuMjt3hfQ== +app-builder-lib@26.0.17: + version "26.0.17" + resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.17.tgz#b23c3722482d3363328248967da3238a44c8da0c" + integrity sha512-fk8edQKtNVnjBUK0kvYEmpbgD3pn3zAwpisjor0KVZLe7kDtnHkaaczjuonshTW+eK1wHhS1W5P2Vv0/u9rwHQ== dependencies: "@develar/schema-utils" "~2.6.5" "@electron/asar" "3.4.1" "@electron/fuses" "^1.8.0" "@electron/notarize" "2.5.0" - "@electron/osx-sign" "1.3.1" + "@electron/osx-sign" "1.3.3" "@electron/rebuild" "3.7.2" - "@electron/universal" "2.0.1" + "@electron/universal" "2.0.3" "@malept/flatpak-bundler" "^0.4.0" "@types/fs-extra" "9.0.13" async-exit-hook "^2.0.1" - builder-util "26.0.13" + builder-util "26.0.17" builder-util-runtime "9.3.2" chromium-pickle-js "^0.2.0" + ci-info "^4.2.0" config-file-ts "0.2.8-rc1" debug "^4.3.4" dotenv "^16.4.5" dotenv-expand "^11.0.6" ejs "^3.1.8" - electron-publish "26.0.13" + electron-publish "26.0.17" fs-extra "^10.1.0" hosted-git-info "^4.1.0" - is-ci "^3.0.0" isbinaryfile "^5.0.0" js-yaml "^4.1.0" json5 "^2.2.3" lazy-val "^1.0.5" - minimatch "^10.0.0" + minimatch "^10.0.3" plist "3.1.0" resedit "^1.7.0" semver "^7.3.8" tar "^6.1.12" temp-file "^3.4.0" tiny-async-pool "1.3.0" + which "^5.0.0" applescript@^1.0.0: version "1.0.0" @@ -736,22 +763,22 @@ builder-util-runtime@9.3.2: debug "^4.3.4" sax "^1.2.4" -builder-util@26.0.13: - version "26.0.13" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.13.tgz#a2c11f8e89e5392719e540d610d70d8413943d74" - integrity sha512-6b64uHzywaL2KAG+rVcqk/Prta1m3I2Jo1d4d2CrApb6EeSk2V384tmSL0EniH+P8jaNbMp6qhg7cIALw32zRA== +builder-util@26.0.17: + version "26.0.17" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.17.tgz#2f000ae7789e0c7c8479397c99d0e25dec3b1d27" + integrity sha512-fym+vg0kegrHBSCmkYYql2EbsLvnlUhIUKRQJ7EHjyftwMz8mibpvTRll3pzK1rtWm/VRdjl7AB397jdtg/Jmw== dependencies: "7zip-bin" "~5.2.0" "@types/debug" "^4.1.6" app-builder-bin "5.0.0-alpha.12" builder-util-runtime "9.3.2" chalk "^4.1.2" + ci-info "^4.2.0" cross-spawn "^7.0.6" debug "^4.3.4" fs-extra "^10.1.0" http-proxy-agent "^7.0.0" https-proxy-agent "^7.0.0" - is-ci "^3.0.0" js-yaml "^4.1.0" sanitize-filename "^1.6.3" source-map-support "^0.5.19" @@ -841,10 +868,10 @@ chromium-pickle-js@^0.2.0: resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" integrity sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw== -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +ci-info@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.2.0.tgz#cbd21386152ebfe1d56f280a3b5feccbd96764c7" + integrity sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg== clean-stack@^2.0.0: version "2.2.0" @@ -1007,6 +1034,17 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-spawn@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1087,7 +1125,7 @@ detect-libc@^2.0.1: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== -detect-newline@^4.0.0: +detect-newline@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== @@ -1105,13 +1143,13 @@ dir-compare@^4.2.0: minimatch "^3.0.5" p-limit "^3.1.0 " -dmg-builder@26.0.14: - version "26.0.14" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.14.tgz#ce8180da319cf3ee05d42cd460a7509207ad474b" - integrity sha512-0l7oEj175hee7NfnaUpb0zf7fsgh1SyHeLjDA0AtOMnBUfTGxPPwrifbUxfd73qzamrGNcyeqza+m/EJx3QUug== +dmg-builder@26.0.17: + version "26.0.17" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.17.tgz#a7cf71fd0f752c8db65f196c91ede4b49d962ec4" + integrity sha512-ls+KKvW4u4/kSkPtdjkTpqSzuVGm90JWRrEulcR/Lu8rrEXaeTcXiesuo3/MWNeiEpwfFgNa8t/paQVH59lw7A== dependencies: - app-builder-lib "26.0.14" - builder-util "26.0.13" + app-builder-lib "26.0.17" + builder-util "26.0.17" builder-util-runtime "9.3.2" fs-extra "^10.1.0" iconv-lite "^0.6.2" @@ -1159,18 +1197,18 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder@^26.0.14: - version "26.0.14" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.14.tgz#8927c6da42a69425d15e08f351e944ea0e7866da" - integrity sha512-YBxpWLMGj0oS7fbS3LVingeZqFunU0F8s+uB9QTd5+wN4qgrf/rSGRkqoImbWg2+F2yHq11wmaA/Xr9xzvgQ0w== +electron-builder@^26.0.17: + version "26.0.17" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.17.tgz#3e252d624aa16031b1c81686ca573180fa437e3d" + integrity sha512-PJbm3XAG9qje73j4iXi043F0JnzHEDP2y/MQBprW+zizZqT2DywTkFN14ryLt8aGkGnoYBP44Smccff0A2AafQ== dependencies: - app-builder-lib "26.0.14" - builder-util "26.0.13" + app-builder-lib "26.0.17" + builder-util "26.0.17" builder-util-runtime "9.3.2" chalk "^4.1.2" - dmg-builder "26.0.14" + ci-info "^4.2.0" + dmg-builder "26.0.17" fs-extra "^10.1.0" - is-ci "^3.0.0" lazy-val "^1.0.5" simple-update-notifier "2.0.0" yargs "^17.6.2" @@ -1180,13 +1218,13 @@ electron-log@^5.4.0: resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.0.tgz#3180bf5194b2e2efacb62ec1392f8150faf4de6b" integrity sha512-AXI5OVppskrWxEAmCxuv8ovX+s2Br39CpCAgkGMNHQtjYT3IiVbSQTncEjFVGPgoH35ZygRm/mvUMBDWwhRxgg== -electron-publish@26.0.13: - version "26.0.13" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.13.tgz#04340520e6e9de5262fecfa011658cfcc3fc8917" - integrity sha512-O5hfHSwli5cegQ4JS3Dp0dZcheex6UCRE/qYyRQvhB6DhSwojiwTnAGEuQCJXc8K8Zxz2lku5Du3VwYHf8d5Lw== +electron-publish@26.0.17: + version "26.0.17" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.17.tgz#c4ed32d39de5796b019fbe8058dd971a6b72371b" + integrity sha512-03hz7MEbzLmZpOCHB+TvoXvn3FW+bZyfgq2gCi4AaeqU6i8Jpx584CljFP8zuDbb0nJcN0uHhpvAWjufDkgyVg== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "26.0.13" + builder-util "26.0.17" builder-util-runtime "9.3.2" chalk "^4.1.2" form-data "^4.0.0" @@ -1202,10 +1240,10 @@ electron-store@^8.2.0: conf "^10.2.0" type-fest "^2.17.0" -electron-updater@^6.6.3: - version "6.6.3" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.3.tgz#a1f53671ffbb08a475d495d48f0c0d971e665d5d" - integrity sha512-i448/SwMtqxy5wqAcXScnWjiFxZp+hmWA2jZCmojcdfodEGhi/DWTdRP01mE3lCILb8hmdE28SBaHf1oQW3+kw== +electron-updater@^6.6.5: + version "6.6.5" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.5.tgz#6614daa2f737c294471eee7ce7b61deda0d5543a" + integrity sha512-jnk38WfByl2Pb0cje02xls/pJkvkq3AQZI7usDCLriU23adkerLTkRrugbCPuUxUOa79nY1g/rokHPWHZFBKyA== dependencies: builder-util-runtime "9.3.2" fs-extra "^10.1.0" @@ -1216,10 +1254,10 @@ electron-updater@^6.6.3: semver "^7.6.3" tiny-typed-emitter "^2.1.0" -electron@^36.1.0: - version "36.1.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-36.1.0.tgz#9919b77e61cd1400acc6dd24f9db8451fba5f8eb" - integrity sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg== +electron@^36.4.0: + version "36.4.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-36.4.0.tgz#9463bf5fa7565ae7be3a274f7f6a46359bcfe74d" + integrity sha512-LLOOZEuW5oqvnjC7HBQhIqjIIJAZCIFjQxltQGLfEC7XFsBoZgQ3u3iFj+Kzw68Xj97u1n57Jdt7P98qLvUibQ== dependencies: "@electron/get" "^2.0.0" "@types/node" "^22.7.7" @@ -1289,7 +1327,7 @@ eslint-scope@^8.0.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -1372,6 +1410,19 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -1438,10 +1489,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== ffmpeg-static@^5.2.0: version "5.2.0" @@ -1589,10 +1640,12 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-stdin@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" - integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" get-stream@^5.1.0: version "5.2.0" @@ -1601,10 +1654,10 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -git-hooks-list@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" - integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== +git-hooks-list@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-4.1.1.tgz#ae340b82a9312354c73b48007f33840bbd83d3c0" + integrity sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA== glob-parent@^5.1.2: version "5.1.2" @@ -1632,7 +1685,7 @@ glob@^10.3.12, glob@^10.3.7: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1830,11 +1883,16 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.4.tgz#a12c70d0f2607c5bf508fb65a40c75f037d7a078" + integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1884,13 +1942,6 @@ ip-address@^9.0.5: jsbn "1.1.0" sprintf-js "^1.1.3" -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-core-module@^2.13.0: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" @@ -1945,6 +1996,11 @@ is-plain-obj@^4.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -1965,6 +2021,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + jackspeak@2.1.1, jackspeak@^3.1.2: version "2.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" @@ -2216,12 +2277,12 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" - integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== dependencies: - brace-expansion "^2.0.1" + "@isaacs/brace-expansion" "^5.0.0" minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" @@ -2244,7 +2305,7 @@ minimatch@^9.0.3, minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -2363,6 +2424,11 @@ next-electron-server@^1.0.0: resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-1.0.0.tgz#03e133ed64a5ef671b6c6409f908c4901b1828cb" integrity sha512-fTUaHwT0Jry2fbdUSIkAiIqgDAInI5BJFF4/j90/okvZCYlyx6yxpXB30KpzmOG6TN/ESwyvsFJVvS2WHT8PAA== +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + node-abi@^3.45.0: version "3.67.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.67.0.tgz#1d159907f18d18e18809dbbb5df47ed2426a08df" @@ -2399,6 +2465,13 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -2463,6 +2536,11 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + p-limit@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2535,6 +2613,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -2599,13 +2682,13 @@ prettier-plugin-organize-imports@^4.1.0: resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f" integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A== -prettier-plugin-packagejson@^2.5.10: - version "2.5.10" - resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7" - integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ== +prettier-plugin-packagejson@^2.5.15: + version "2.5.15" + resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.15.tgz#7ea880d4bb1681b5331ea7044efd3d653776f469" + integrity sha512-2QSx6y4IT6LTwXtCvXAopENW5IP/aujC8fobEM2pDbs0IGkiVjW/ipPuYAHuXigbNe64aGWF7vIetukuzM3CBw== dependencies: - sort-package-json "2.15.1" - synckit "0.9.2" + sort-package-json "3.2.1" + synckit "0.11.8" prettier@3.5.3: version "3.5.3" @@ -2829,6 +2912,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.7.1: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -2836,6 +2924,13 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2843,6 +2938,11 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -2853,24 +2953,25 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== +shelljs@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.9.2.tgz#a8ac724434520cd7ae24d52071e37a18ac2bb183" + integrity sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw== dependencies: - glob "^7.0.0" + execa "^1.0.0" + fast-glob "^3.3.2" interpret "^1.0.0" rechoir "^0.6.2" -shx@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" - integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== +shx@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.4.0.tgz#c6ea6ace7e778da0ab32d2eab9def59d788e9336" + integrity sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA== dependencies: - minimist "^1.2.3" - shelljs "^0.8.5" + minimist "^1.2.8" + shelljs "^0.9.2" -signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -2923,19 +3024,18 @@ sort-object-keys@^1.1.3: resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -sort-package-json@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e" - integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA== +sort-package-json@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.2.1.tgz#889f3bdf43ceeff5fa4278a7c53ae5b1520d287e" + integrity sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg== dependencies: detect-indent "^7.0.1" - detect-newline "^4.0.0" - get-stdin "^9.0.0" - git-hooks-list "^3.0.0" + detect-newline "^4.0.1" + git-hooks-list "^4.0.0" is-plain-obj "^4.1.0" - semver "^7.6.0" + semver "^7.7.1" sort-object-keys "^1.1.3" - tinyglobby "^0.2.9" + tinyglobby "^0.2.12" source-map-support@^0.5.19: version "0.5.21" @@ -2990,6 +3090,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3021,13 +3126,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" - integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== +synckit@0.11.8: + version "0.11.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457" + integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== dependencies: - "@pkgr/core" "^0.1.0" - tslib "^2.6.2" + "@pkgr/core" "^0.2.4" tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1: version "6.2.1" @@ -3078,12 +3182,12 @@ tiny-typed-emitter@^2.1.0: resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== -tinyglobby@^0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f" - integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== +tinyglobby@^0.2.12: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== dependencies: - fdir "^6.4.2" + fdir "^6.4.4" picomatch "^4.0.2" tmp-promise@^3.0.2: @@ -3117,12 +3221,12 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-api-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" - integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.1.0, tslib@^2.6.2: +tslib@^2.1.0: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== @@ -3149,14 +3253,14 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.31.1: - version "8.31.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b" - integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA== +typescript-eslint@^8.33.1: + version "8.33.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.33.1.tgz#d2d59c9b24afe1f903a855b02145802e4ae930ff" + integrity sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A== dependencies: - "@typescript-eslint/eslint-plugin" "8.31.1" - "@typescript-eslint/parser" "8.31.1" - "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/eslint-plugin" "8.33.1" + "@typescript-eslint/parser" "8.33.1" + "@typescript-eslint/utils" "8.33.1" typescript@^5.4.3, typescript@^5.8.3: version "5.8.3" @@ -3230,6 +3334,13 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3237,6 +3348,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +which@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-5.0.0.tgz#d93f2d93f79834d4363c7d0c23e00d07c466c8d6" + integrity sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ== + dependencies: + isexe "^3.1.1" + winreg@1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" @@ -3311,3 +3429,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.25.51: + version "3.25.51" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.51.tgz#aa2cf648e54f6f060f139cf77b694819f63c9f3a" + integrity sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg== diff --git a/docs/docs/.vitepress/config.ts b/docs/docs/.vitepress/config.ts index a2df3c23a1..4914ab6251 100644 --- a/docs/docs/.vitepress/config.ts +++ b/docs/docs/.vitepress/config.ts @@ -8,13 +8,6 @@ export default defineConfig({ head: [["link", { rel: "icon", type: "image/png", href: "/favicon.png" }]], cleanUrls: true, ignoreDeadLinks: "localhostLinks", - vite: { - build: { - rollupOptions: { - external: ['client-museum-s3.png'] // Added to handle static asset import - } - } - }, themeConfig: { // We use the default theme (with some CSS color overrides). This // themeConfig block can be used to further customize the default theme. diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index 02b8fdc5e9..a90caad19b 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -52,6 +52,10 @@ export const sidebar = [ link: "/photos/features/machine-learning", }, { text: "Map", link: "/photos/features/map" }, + { + text: "Notifications", + link: "/photos/features/notifications", + }, { text: "Passkeys", link: "/photos/features/passkeys", @@ -264,34 +268,27 @@ export const sidebar = [ collapsed: true, items: [ { text: "Introduction", link: "/self-hosting/guides/" }, - { - text: "Hosting Ente from source", - link: "/self-hosting/guides/from-source", - }, - { - text: "Hosting Ente without Docker", - link: "/self-hosting/guides/standalone-ente", - }, { text: "Administering your server", link: "/self-hosting/guides/admin", }, { - text: "Configure CLI for self hosted instance", + text: "Configuring CLI for your instance", link: "/self-hosting/guides/selfhost-cli", }, { - text: "DB migration", - link: "/self-hosting/guides/db-migration", + text: "Running Ente from source", + link: "/self-hosting/guides/from-source", }, { - text: "Mobile build", - link: "/self-hosting/guides/mobile-build", + text: "Running Ente without Docker", + link: "/self-hosting/guides/standalone-ente", }, ], }, { text: "Troubleshooting", + collapsed: true, items: [ { text: "General", @@ -299,7 +296,7 @@ export const sidebar = [ }, { text: "Bucket CORS", - link: '/self-hosting/troubleshooting/bucket-cors' + link: "/self-hosting/troubleshooting/bucket-cors", }, { text: "Uploads", @@ -309,10 +306,6 @@ export const sidebar = [ text: "Docker / quickstart", link: "/self-hosting/troubleshooting/docker", }, - { - text: "Yarn", - link: "/self-hosting/troubleshooting/yarn", - }, { text: "Ente CLI secrets", link: "/self-hosting/troubleshooting/keyring", @@ -321,19 +314,21 @@ export const sidebar = [ }, { text: "Community Guides", - items :[ + collapsed: true, + items: [ { text: "Ente via Tailscale", - link: "/self-hosting/guides/Tailscale", + link: "/self-hosting/guides/tailscale", }, { text: "Ente with External S3", link: "/self-hosting/guides/external-s3", - } - ] + }, + ], }, { text: "FAQ", + collapsed: true, items: [ { text: "General", link: "/self-hosting/faq/" }, { @@ -348,16 +343,12 @@ export const sidebar = [ text: "Backups", link: "/self-hosting/faq/backup", }, + { + text: "Environment variables", + link: "/self-hosting/faq/environment", + }, ], }, ], }, - { - text: "About", - link: "/about/", - }, - { - text: "Contribute", - link: "/about/contribute", - }, ]; diff --git a/docs/docs/about/contribute.md b/docs/docs/about/contribute.md deleted file mode 100644 index 3addd2e606..0000000000 --- a/docs/docs/about/contribute.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Contribute -description: Details about how to contribute to Ente's docs ---- - -# Contributing - -To contribute to these docs, you can use the "Edit this page" button at the -bottom of each page. This will allow you to directly edit the markdown file that -is used to generate this documentation and open a quick pull request directly -from GitHub's UI. - -If you're more comfortable in contributing with your text editor, see the -`docs/` folder of our GitHub repository, -[github.com/ente-io/ente](https://github.com/ente-io/ente). diff --git a/docs/docs/about/index.md b/docs/docs/about/index.md deleted file mode 100644 index 18455f6b5a..0000000000 --- a/docs/docs/about/index.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: About Ente -description: > - An overview of Ente: the company, and the people behind it, and the products - that we make. ---- - -# About - -Ente is a end-to-end encrypted platform for privately, reliably, and securely -storing your data on the cloud. On top of this platform, Ente offers two -products: - -- **Ente Photos** - An alternative to Google Photos and Apple Photos - -- **Ente Auth** - A free 2FA alternative to Authy - -Both these apps are available for all desktop (Linux, Mac, Windows) and mobile -(Android, iOS and F-Droid) platforms. They also work directly in your web -browser without you needing to install anything. - -More products are in the pipeline. - -## History - -Ente was the founded by Vishnu Mohandas (he's also Ente's CEO) in response to -privacy concerns with major tech companies. The underlying motivation was the -understanding that big tech had no incentive to fix their act, but with -end-to-end encrypted cross platform apps, there was a way for people to take -back control over their own data without sacrificing on features. - -### What does Ente mean? - -In Malayalam, Vishnu's native language, "ente" means "mine". Thus "Ente Photos" -has the literal meaning "my photos". - -This was a good name, but still Vishnu looked around for better ones. But one -day, he discovered that "ente" means "duck" in German. This unexpected -connection sealed the deal. We should ask him why he likes ducks so much, but -apparently he does, so this dual meaning ("mine" / "duck") led him to finalize -the name, and also led to the adoption of "Ducky", Ente's mascot: - -
- -![Ente's mascot, Ducky](ducky.png){width=200px} - -
- -For the full origin story of Ducky you can check out -[this blog post](https://ente.io/blog/ducky/). - -### How do I pronounce Ente? - -en-_tay_. Like cafe. - -## Get in touch - -If you have a support query that is not answered by these docs, please reach out -to our Customer Support by sending an email to support@ente.io - -To stay up to date with new product launches, and behind the scenes details of -how we're building Ente, you can read our [blog](https://ente.io/blog) (or -subscribe to it via [RSS](https://ente.io/blog/rss.xml)) - -To suggest new features and/or offer your perspective on how we should design -planned and upcoming features, use our -[GitHub discussions](https://github.com/ente-io/ente/discussions) - -Or if you'd just like to hang out, join our -[Discord](https://discord.gg/z2YVKkycX3), follow us on -[Twitter](https://twitter.com/enteio) or give us a shout out on -[Mastodon](https://mstdn.social/@ente) diff --git a/docs/docs/auth/faq/index.md b/docs/docs/auth/faq/index.md index 5e631906aa..b731fa573e 100644 --- a/docs/docs/auth/faq/index.md +++ b/docs/docs/auth/faq/index.md @@ -41,6 +41,16 @@ Usually, this discrepancy occurs because the time in your browser might be incorrect. In particular, multiple users have reported that Firefox provides incorrect time when certain privacy settings are enabled. +> [!TIP] +> +> Newer Ente Auth clients (upcoming 4.4.0+) will automatically try to correct +> for incorrect system time, so you should be seeing correct codes even if your +> system time is out of sync. However, this automatic correction will not work +> if you're using Ente Auth in offline mode. +> +> If you've recently changed your system time and the codes are still incorrect, +> try to refresh / restart the app if needed. + ### Can I access my codes on web? You can access your codes on the web at [auth.ente.io](https://auth.ente.io). diff --git a/docs/docs/auth/migration-guides/authy/index.md b/docs/docs/auth/migration-guides/authy/index.md index 27baa97ab1..724fbdc1d2 100644 --- a/docs/docs/auth/migration-guides/authy/index.md +++ b/docs/docs/auth/migration-guides/authy/index.md @@ -10,8 +10,9 @@ A guide written by Green, an ente.io lover > [!WARNING] > > Authy has dropped all support for its desktop apps. It is no longer possible -> to export data from Authy using methods 1 and 2. You will need either an iOS device -> and computer (method 4) or a rooted Android phone (method 3) to follow this guide. +> to export data from Authy using methods 1 and 2. You will need either an iOS +> device and computer (method 4) or a rooted Android phone (method 3) to follow +> this guide. --- @@ -204,11 +205,24 @@ This uses the tool [Aegis Authenticator](https://getaegis.app/) from ## Method 4: Authy-iOS-MiTM -**Who should use this?** Technical iOS users of Authy that cannot export their tokens with methods 1 or 2 (due to those methods being patched) or method 3 (due to that method requiring a rooted Android device). +**Who should use this?** Technical iOS users of Authy that cannot export their +tokens with methods 1 or 2 (due to those methods being patched) or method 3 (due +to that method requiring a rooted Android device). -This method works by intercepting the data the Authy app receives while logging in for the first time, which contains your encrypted authenticator tokens. After the encrypted authenticator tokens are dumped, you can decrypt them using your backup password and convert them to an Ente token file. +This method works by intercepting the data the Authy app receives while logging +in for the first time, which contains your encrypted authenticator tokens. After +the encrypted authenticator tokens are dumped, you can decrypt them using your +backup password and convert them to an Ente token file. -For an up-to-date guide of how to retrieve the encrypted authenticator tokens and decrypt them, please see [Authy-iOS-MiTM](https://github.com/AlexTech01/Authy-iOS-MiTM). To convert the `decrypted_tokens.json` file from that guide into a format Ente Authenticator can recognize, use [this](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93?permalink_comment_id=5317087#gistcomment-5317087) Python script. Once you have the `ente_auth_import.plain` file from that script, transfer it to your device and follow the instructions below to import it into Ente Authenticator. +For an up-to-date guide of how to retrieve the encrypted authenticator tokens +and decrypt them, please see +[Authy-iOS-MiTM](https://github.com/AlexTech01/Authy-iOS-MiTM). To convert the +`decrypted_tokens.json` file from that guide into a format Ente Authenticator +can recognize, use +[this](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93?permalink_comment_id=5317087#gistcomment-5317087) +Python script. Once you have the `ente_auth_import.plain` file from that script, +transfer it to your device and follow the instructions below to import it into +Ente Authenticator. ## Importing to Ente Authenticator (Method 1, method 2.1, method 4) diff --git a/docs/docs/de/auth/index.md b/docs/docs/de/auth/index.md index 1948a9126d..cb96f33738 100644 --- a/docs/docs/de/auth/index.md +++ b/docs/docs/de/auth/index.md @@ -10,4 +10,4 @@ Ende-zu-Ende-verschlüsselte Authenticator-App für jedermann. Wir sind froh, da du hier bist! **Please note that this German translation is currently just a placeholder.** -Know German? [Help us fill this in!](/about/contribute). +Know German? [Help us fill this in!](/#contribute). diff --git a/docs/docs/index.md b/docs/docs/index.md index af5fd2cddf..6bf6d18275 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,15 +1,82 @@ --- title: Home +description: > + Introduction to Ente: Products, Community and Support --- # Welcome! -This site contains documentation and help for Ente Photos and Ente Auth. It -describes various features, and also offers various troubleshooting suggestions. +![Ducky: Ente's Mascot](/public/ducky.png){width=50% style="margin: 0 auto"} -Use the **sidebar** menu to navigate to information about the product (Photos or -Auth) you'd like to know more about. Or use the **search** at the top to try and -jump directly to page that might contain the information you need. +## Introduction -To know more about Ente, see [about](/about/) or visit our website -[ente.io](https://ente.io). +Ente (pronounced en-_tay_) is a end-to-end encrypted platform for privately, +reliably, and securely storing your data on the cloud, over which 2 applications +have been developed and made available for mobile, web and desktop, namely: + +- **Ente Photos** - An alternative to Google Photos and Apple Photos +- **Ente Auth** - A free 2FA alternative to Authy + +## History + +Ente was the founded by Vishnu Mohandas (he's also Ente's CEO) in response to +privacy concerns with major tech companies. The underlying motivation was the +understanding that big tech had no incentive to fix their act, but with +end-to-end encrypted cross platform apps, there was a way for people to take +back control over their own data without sacrificing on features. + +### Origin of the name + +In Malayalam, Vishnu's native language, "ente" means "mine", thus "Ente Photos" +literally means "my photos". + +But one day, he discovered that "ente" means "duck" in German. This unexpected +connection sealed the deal after looking for alternative names and led to the +adoption of ["Ducky"](https://ente.io/blog/ducky/), representing the playfulness +and friendly nature of the community and team. + +## Getting Started + +We recommend reading the documentation for [Ente Photos](/photos/) or +[Ente Auth](/auth/) to get started with installation on the desired platform, +explore available features and usage. + +If you are looking to self-host Ente, we recommend you to read the +[official documentation](/self-hosting/) for updated information on getting +started, installation, administration and maintenance. + +## Contributing + +There are many ways to support Ente and you don't have to be a programmer for +that. You can spread the word, give feedback, report bugs, help us with +translations, contribute documentation and community guides and more. + +To suggest new features and/or offer your perspective on how we should design +(planned and upcoming features), use our +[GitHub discussions](https://github.com/ente-io/ente/discussions) + +You can find our contribution guidelines +[here](https://github.com/ente-io/ente/blob/main/CONTRIBUTING.md). + +You can always engage with our community and team to hang out, answer queries +and stay updated: + +- Chat: [Discord](https://ente.io/discord) +- Discussions: [GitHub](https://github.com/ente-io/ente/discussions) +- Socials: + - Twitter: [enteio](https://twitter.com/enteio) + - Mastodon: [@ente@fosstodon.org](https://fosstodon.org/@ente) + - Bluesky: [ente.io](https://bsky.app/profile/ente.io) + - Instagram: [ente.app](https://www.instagram.com/ente.app) +- Website: + - [Blog](https://ente.io/blog) + - [RSS](https://ente.io/blog/rss.xml) + +## Getting Help + +If you encounter any issues with any of the products that's not answered by our +documentation, please reach out to our team by sending an email to +[support@ente.io](mailto:support@ente.io) + +For community support, please post your queries on +[Discord](https://discord.gg/z2YVKkycX3) diff --git a/docs/docs/photos/faq/desktop.md b/docs/docs/photos/faq/desktop.md index 71a575bdfb..c0ef805584 100644 --- a/docs/docs/photos/faq/desktop.md +++ b/docs/docs/photos/faq/desktop.md @@ -1,6 +1,7 @@ --- title: Desktop app FAQ -description: An assortment of frequently asked questions about Ente Photos desktop app +description: + An assortment of frequently asked questions about Ente Photos desktop app --- # Desktop app FAQ @@ -15,7 +16,8 @@ to manually update the software. ### Upload errors -**How do I identify which files experienced upload issues within the desktop app?** +**How do I identify which files experienced upload issues within the desktop +app?** Check the sections within the upload progress bar for "Failed Uploads," "Ignored Uploads," and "Unsuccessful Uploads." @@ -33,6 +35,5 @@ be specific to your distro (e.g. `xdg-desktop-menu forceupdate`). > [!NOTE] > -> If you're using an AppImage and not seeing the icon, you'll need to [enable -> AppImage desktop -> integration](/photos/troubleshooting/desktop-install/#appimage-desktop-integration). +> If you're using an AppImage and not seeing the icon, you'll need to +> [enable AppImage desktop integration](/photos/troubleshooting/desktop-install/#appimage-desktop-integration). diff --git a/docs/docs/photos/faq/export.md b/docs/docs/photos/faq/export.md index 16879ee327..5f37640f83 100644 --- a/docs/docs/photos/faq/export.md +++ b/docs/docs/photos/faq/export.md @@ -7,9 +7,10 @@ description: Frequently asked questions about keeping extra backups of your data ## How can I backup my data in a local drive outside Ente? -You can use our CLI tool or our desktop app to set up exports of your data -to your local drive. This way, you can use Ente in your day to day use, with an additional guarantee that a copy of your original photos and videos are -always available on your machine. +You can use our CLI tool or our desktop app to set up exports of your data to +your local drive. This way, you can use Ente in your day to day use, with an +additional guarantee that a copy of your original photos and videos are always +available on your machine. - You can use [Ente's CLI](https://github.com/ente-io/ente/tree/main/cli#export) to export your data in a cron job to a location of your choice. The exports @@ -21,7 +22,7 @@ always available on your machine. background without you needing to run any other cron jobs. See [migration/export](/photos/migration/export/) for more details. -## Does the exported data preserve folder structure? +## Does the exported data preserve album structure? Yes. When you export your data for local backup, it will maintain the exact album structure how you have set up within Ente. diff --git a/docs/docs/photos/faq/face-recognition.md b/docs/docs/photos/faq/face-recognition.md index 24c9650345..8559bf310d 100644 --- a/docs/docs/photos/faq/face-recognition.md +++ b/docs/docs/photos/faq/face-recognition.md @@ -1,7 +1,6 @@ --- title: Face recognition -description: - Frequently asked questions about Ente's face recognition +description: Frequently asked questions about Ente's face recognition --- # Face recognition diff --git a/docs/docs/photos/faq/general.md b/docs/docs/photos/faq/general.md index d45bf292f7..040a7e1bfa 100644 --- a/docs/docs/photos/faq/general.md +++ b/docs/docs/photos/faq/general.md @@ -26,10 +26,6 @@ unsupported file format and we will do our best to help you out. Yes, we currently do not support files larger than 4 GB. -If this constraint is a concern for you, please write to -[support@ente.io](mailto:support@ente.io) with your use case and we will do our -best to help you. - ## Does Ente support videos? Ente supports backing up and downloading of videos in their original format and @@ -104,29 +100,53 @@ clicking on "Your map" under "Locations" on the search screen. ## How to reset my password if I lost it? -On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password. +On the login page, enter your email and click on Forgot Password. Then, enter +your recovery key and create a new password. - # iOS Album Backup and Organization in Ente +## Can I search for photos using the descriptions I’ve added? - ### How does Ente handle photos that are part of multiple iOS albums? -When you select multiple albums for backup, Ente prioritizes uploading each photo to the album with the fewest photos. This means a photo will only be uploaded once, even if it exists in multiple albums on your device. If you create new albums on your device after the initial backup, those photos may not appear in the corresponding Ente album if they were already uploaded to a different album. +Yes, descriptions are searchable, making it easier to find specific photos +later. To do this, open the photo, tap the (i) button, and enter your +description. +## How does the deduplication feature work on the desktop app? -### Why don’t all photos from a new iOS album appear in the corresponding Ente album? -If you create a new album on your device after the initial backup, the photos in that album may have already been uploaded to another album in Ente. To fix this, go to the "On Device" album in Ente, select all photos, and manually add them to the corresponding album in Ente. +If the app finds exact duplicates, it will show them in the deduplication. When +you delete a duplicate, the app keeps one copy and creates a symlink for the +other duplicate. This helps save storage space. -### What happens if I reorganize my photos in the iOS Photos app after backing up? -Reorganizing photos in the iOS Photos app (e.g., moving photos to new albums) won’t automatically reflect in Ente. You’ll need to manually add those photos to the corresponding albums in Ente to maintain consistency. +## What happens if I lose access to my email address? Can I use my recovery key to bypass email verification? -### Can I search for photos using the descriptions I’ve added? -Yes, descriptions are searchable, making it easier to find specific photos later. -To do this, open the photo, tap the (i) button, and enter your description. - -### How does the deduplication feature work on the desktop app? -If the app finds exact duplicates, it will show them in the deduplication. When you delete a duplicate, the app keeps one copy and creates a symlink for the other duplicate. This helps save storage space. - -### What happens if I lose access to my email address? Can I use my recovery key to bypass email verification? -No, the recovery key does not bypass email verification. For security reasons, we do not disable or bypass email verification unless the account owner reaches out to us and successfully verifies their identity by providing details about their account. +No, the recovery key does not bypass email verification. For security reasons, +we do not disable or bypass email verification unless the account owner reaches +out to us and successfully verifies their identity by providing details about +their account. If you lose access to your email, please contact our support team at support@ente.io + +--- + +# iOS Album Backup and Organization in Ente + +## How does Ente handle photos that are part of multiple iOS albums? + +When you select multiple albums for backup, Ente prioritizes uploading each +photo to the album with the fewest photos. This means a photo will only be +uploaded once, even if it exists in multiple albums on your device. If you +create new albums on your device after the initial backup, those photos may not +appear in the corresponding Ente album if they were already uploaded to a +different album. + +## Why don’t all photos from a new iOS album appear in the corresponding Ente album? + +If you create a new album on your device after the initial backup, the photos in +that album may have already been uploaded to another album in Ente. To fix this, +go to the "On Device" album in Ente, select all photos, and manually add them to +the corresponding album in Ente. + +## What happens if I reorganize my photos in the iOS Photos app after backing up? + +Reorganizing photos in the iOS Photos app (e.g., moving photos to new albums) +won’t automatically reflect in Ente. You’ll need to manually add those photos to +the corresponding albums in Ente to maintain consistency. diff --git a/docs/docs/photos/faq/metadata.md b/docs/docs/photos/faq/metadata.md index 03cf10da65..c7e122301b 100644 --- a/docs/docs/photos/faq/metadata.md +++ b/docs/docs/photos/faq/metadata.md @@ -62,6 +62,7 @@ the upload time as the photo's creation time. ## Modifications Ente supports modifications to the following metadata: + - File name - Date & time - Location diff --git a/docs/docs/photos/faq/video-streaming.md b/docs/docs/photos/faq/video-streaming.md index 59a34907f6..62d9764dbb 100644 --- a/docs/docs/photos/faq/video-streaming.md +++ b/docs/docs/photos/faq/video-streaming.md @@ -1,29 +1,49 @@ --- title: Video streaming FAQ -description: - Frequently asked questions about Ente's video streaming feature +description: Frequently asked questions about Ente's video streaming feature --- # Video streaming > [!NOTE] > -> Video streaming is available in beta on mobile apps starting v0.9.98. +> Video streaming is available in beta on mobile apps starting v0.9.98 and on +> desktop starting v1.7.13. ### How to enable video streaming? +#### On mobile + - Open Settings -> General -> Advanced -- Switch on the toggle for `Video streaming` +- Enable the toggle for `Streamable videos` + +#### On desktop + +- Open Settings -> Preferences +- Enable the toggle for `Streamable videos` ### What happens when I enable video streaming? +#### On mobile + Enabling video streaming will start processing videos captured in the last 30 days, generating streams for each. Both local and remote videos will be processed, so this may consume bandwidth for downloading of remote files and uploading of the generated streams. +#### On desktop + +When enabled, the desktop app will generate streams both for new uploads, and +also for all existing videos that were previously uploaded. + +Stream generation is CPU intensive and can take time so the app will continue +processing them in the background. Clicking on search bar will show "Processing +videos..." when stream generation is happening. + ### How can I view video streams? +### On mobile + Settings -> Backup > Backup status will show details regarding the processing status for videos. Processed videos will have a green play button next to them. You can open these videos by tapping on them. @@ -34,6 +54,12 @@ play the stream. Clicking on the `Info` icon within the original video will show details about the generated stream. +### On desktop and web + +Desktop and web app will automatically play the streaming version of a video if +it has been already generated. The quality selector will show "Auto" when +playing the stream. + ### What is a stream? Stream is an encrypted HLS file with an `.m3u8` playlist that helps play a video @@ -51,6 +77,7 @@ generated stream. While this feature is in beta, we will not count the storage consumed by your streams against your storage quota. This may change in the future. If it does, we will provide an option to opt-in to one of the following: + 1. Original videos only 2. Compressed streams only 3. Both diff --git a/docs/docs/photos/features/background.md b/docs/docs/photos/features/background.md index ffed413f74..2decf9cb65 100644 --- a/docs/docs/photos/features/background.md +++ b/docs/docs/photos/features/background.md @@ -43,8 +43,8 @@ need to disable this "Optimize battery usage" mode in the system settings for Ente if you wish for Ente to automatically back up your photos in the background. -On Android versions 15 and later, if an app is in private space and the private -space is locked, Android doesn’t allow the app to run any background processes. +On Android versions 15 and later, if an app is in private space and the private +space is locked, Android doesn’t allow the app to run any background processes. As a result, background sync will not work. ### Desktop diff --git a/docs/docs/photos/features/deduplicate.md b/docs/docs/photos/features/deduplicate.md index c1b55e4cc7..ea931d174d 100644 --- a/docs/docs/photos/features/deduplicate.md +++ b/docs/docs/photos/features/deduplicate.md @@ -52,6 +52,11 @@ Ente also provides a tool for manual de-duplication in _Settings → Backup → Remove duplicates_. This is useful if you have an existing library with duplicates across different albums, but wish to keep only one copy. +During this operation, Ente will discard duplicates across all albums, retain a +single copy, and add symlinks to this copy within all existing albums. So your +existing album structure remains unchanged, while the space consumed by the +duplicate data is freed up. + ## Adding to Ente album creates symlinks Note that once a file is in Ente, adding it to another Ente album will create a diff --git a/docs/docs/photos/features/family-plans.md b/docs/docs/photos/features/family-plans.md index 711b9e3094..cc5ee52aff 100644 --- a/docs/docs/photos/features/family-plans.md +++ b/docs/docs/photos/features/family-plans.md @@ -24,19 +24,19 @@ In brief, ## Storage Limits -If you're an admin of a family, you will be able to set storage limits for the +If you're an admin of a family, you will be able to set storage limits for the members in your family plan. -In brief, +In brief, -- For example, once you set a limit of 10GB for a member, their Storage - quota for uploading photos will be limited to 10GB. +- For example, once you set a limit of 10GB for a member, their Storage quota + for uploading photos will be limited to 10GB. -- Once the invited member accepts the Family invite, you will be able to see - an edit icon in the Members List. Click on it to setup a family limit. +- Once the invited member accepts the Family invite, you will be able to see an + edit icon in the Members List. Click on it to setup a family limit. - If the admin has set a limit for any user, that limit value will be prefilled - in the input box. + in the input box. -- If you want to remove any storage limit from a members account, you - can click on the "Remove Limit" and they can upload photos without any limit. +- If you want to remove any storage limit from a members account, you can click + on the "Remove Limit" and they can upload photos without any limit. diff --git a/docs/docs/photos/features/machine-learning.md b/docs/docs/photos/features/machine-learning.md index 215c1d98a8..e43edd5b43 100644 --- a/docs/docs/photos/features/machine-learning.md +++ b/docs/docs/photos/features/machine-learning.md @@ -47,8 +47,20 @@ device. The indexes are synced across all your devices automatically using the same end-to-end encrypted security that we use for syncing your photos. -Note that the desktop app does not currently support modifying the face -groupings, that is only supported by the mobile app. +--- + +#### Local indexing on mobile + +In general the machine learning is optimized to work well on most mobile device. +However, devices with low RAM (4-6GB) and large photo libraries might struggle +to complete the indexing without affecting performance of the app. In such case, +you might want to disable local indexing and let the desktop run it instead. + +You can disable local indexing from the settings, under +`General > Advanced > Machine learning > Configuration`. This way, you can +continue to use the ML features without your phone performance taking any hit. + +--- For more information on how to use Machine Learning for face recognition please check out [the FAQ](../faq/face-recognition). diff --git a/docs/docs/photos/features/notifications.md b/docs/docs/photos/features/notifications.md new file mode 100644 index 0000000000..0cf33a6e0e --- /dev/null +++ b/docs/docs/photos/features/notifications.md @@ -0,0 +1,33 @@ +--- +title: Notifications +description: Details about notifications in Ente +--- + +# Notifications + +The Ente app can send notifications to notify you of an update, or just to +remind you of some sweet or helpful memory at the right time. + +## New shared photos + +Receive notifications when someone adds a photo to a shared album that you're a +part of. + +## "On this day" memories + +Receive reminders about memories from this day in previous years. These +reminders will only be shown if there are enough photos taken across previous +years of the specific day. + +## Birthday notifications + +Receive reminders when it's someone's birthday. Tapping on the notification will +take you to photos of the birthday person. This requires you to first add a +birthday to a person, and will only be shown if there are enough photos of that +person. + +## Notification permission + +By default all notification categories are enabled if you give notification +permission. You can disable all of the above notification categories from +`Settings > Notifications`. Notifications currently only work on mobile. diff --git a/docs/docs/photos/migration/export/index.md b/docs/docs/photos/migration/export/index.md index 39d14984ed..1e00623e54 100644 --- a/docs/docs/photos/migration/export/index.md +++ b/docs/docs/photos/migration/export/index.md @@ -66,5 +66,4 @@ If you run into any issues during your data export, please reach out to Note that we also provide a [CLI tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your -data. You can find more information about the export in the -[export FAQ](/photos/faq/export). +data. diff --git a/docs/docs/photos/troubleshooting/nas.md b/docs/docs/photos/troubleshooting/nas.md index 262d4c758d..c84463ce63 100644 --- a/docs/docs/photos/troubleshooting/nas.md +++ b/docs/docs/photos/troubleshooting/nas.md @@ -14,9 +14,21 @@ directly stream chunks of Google Takeout zips that are stored on network drives. In particular, the folder watch functionality suffers a lot since the app needs access to file system events to detect changes to the users files so that they -can be uploaded whenever there are changes. +can be uploaded whenever there are changes. Network drives are less reliable in +providing these file change events correctly. Since are high chances of the user having a subpar experience, we request customers to avoid using the desktop app directly with network attached storage and instead temporarily copy the files to their local storage for uploads, and avoid watching folders that live on a network drive. + +## Exporting to UNC paths + +Generally, exports are likely to work better than imports, since the interaction +with the file system is relatively simpler (Note that the app still needs to +scan the folder to find existing files, esp. if the continuous export option is +enabled). + +A special case is when exporting to a UNC path. In this case, the file +separators will not work as expected and the export will not start. As a +workaround, you can map your UNC path to a network drive and use that instead. diff --git a/docs/docs/about/ducky.png b/docs/docs/public/ducky.png similarity index 100% rename from docs/docs/about/ducky.png rename to docs/docs/public/ducky.png diff --git a/docs/docs/self-hosting/creating-accounts.md b/docs/docs/self-hosting/creating-accounts.md index 5c3a53d3a3..550409106e 100644 --- a/docs/docs/self-hosting/creating-accounts.md +++ b/docs/docs/self-hosting/creating-accounts.md @@ -3,7 +3,7 @@ title: Creating accounts description: Creating accounts on your deployment --- -# Creating accounts +# Creating accounts Once Ente is up and running, the Ente Photos web app will be accessible on `http://localhost:3000`. Open this URL in your browser and proceed with creating @@ -20,7 +20,7 @@ This code can be found in the server logs, which should already be shown in your quickstart terminal. Alternatively, you can open the server logs with the following command from inside the `my-ente` folder: -```sh +```sh sudo docker compose logs ``` diff --git a/docs/docs/self-hosting/faq/environment.md b/docs/docs/self-hosting/faq/environment.md new file mode 100644 index 0000000000..a8dcf18d06 --- /dev/null +++ b/docs/docs/self-hosting/faq/environment.md @@ -0,0 +1,52 @@ +--- +title: "Environment Variables and Ports" +description: + "Information about all the Environment Variables needed to run Ente" +--- + +# Environment variables and ports + +A self-hosted Ente instance requires specific endpoints in both Museum (the +server) and web apps. This document outlines the essential environment variables +and port mappings of the web apps. + +Here's the list of important variables that a self hoster should know about: + +### Museum + +1. `NEXT_PUBLIC_ENTE_ENDPOINT` + +The above environment variable is used to configure Museums endpoint. Where +Museum is running and which port it is listening on. This endpoint should be +configured for all the apps to connect to your self hosted endpoint. + +All the apps (regardless of platform) by default connect to api.ente.io - which +is our production instance of Museum. + +### Web Apps + +> [!IMPORTANT] Web apps don't need to be configured with the below endpoints. +> Web app environment variables are being documented here just so that the users +> know everything in detail. Checkout +> [Configuring your Server](/self-hosting/museum) to configure endpoints for +> particular app. + +In Ente, all the web apps are separate NextJS applications. Therefore, they are +all configured via environment variables. The photos app (Ente Photos) has +information about and connects to other web apps like albums, cast, etc. + +1. `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` + +This environment variable is used to configure and declare the endpoint for the +Albums web app. + +## Ports + +The below format is according to how ports are mapped in Docker. +Typically,`:` + +1. `8080:8080`: Museum (Ente's server) +2. `3000:3000`: Ente Photos web app +3. `3001:3001`: Ente Accounts web app +4. `3003:3003`: [Ente Auth web app](https://ente.io/auth/) +5. `3004:3004`: [Ente Cast web app](http://ente.io/cast) diff --git a/docs/docs/self-hosting/guides/admin.md b/docs/docs/self-hosting/guides/admin.md index 75048f2804..10d05fb4d0 100644 --- a/docs/docs/self-hosting/guides/admin.md +++ b/docs/docs/self-hosting/guides/admin.md @@ -3,32 +3,6 @@ title: Server admin description: Administering your custom self-hosted Ente instance using the CLI --- -# Administering your custom server - -You can use -[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to -administer your self hosted server. - -First we need to get your CLI to connect to your custom server. Define a -config.yaml and put it either in the same directory as CLI or path defined in -env variable `ENTE_CLI_CONFIG_PATH` - -```yaml -endpoint: - api: "http://localhost:8080" -``` - -Now you should be able to -[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), -and subsequently increase the -[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) -using the CLI. - -> [!NOTE] -> -> The CLI command to add an account does not create Ente accounts. It only adds -> existing accounts to the list of (existing) accounts that the CLI can use. - ## Becoming an admin By default, the first user (and only the first user) created on the system is @@ -78,6 +52,37 @@ You can use [account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md) command to find the user id of any account. +# Administering your custom server + +> [!NOTE] For the first user (admin) to perform administrative actions using the +> CLI, their userID must be whitelisted in the `museum.yaml` configuration file +> under `internal.admins`. While the first user is automatically granted admin +> privileges on the server, this additional step is required for CLI operations. + +You can use +[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to +administer your self hosted server. + +First we need to get your CLI to connect to your custom server. Define a +config.yaml and put it either in the same directory as CLI or path defined in +env variable `ENTE_CLI_CONFIG_PATH` + +```yaml +endpoint: + api: "http://localhost:8080" +``` + +Now you should be able to +[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), +and subsequently increase the +[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) +using the CLI. + +> [!NOTE] +> +> The CLI command to add an account does not create Ente accounts. It only adds +> existing accounts to the list of (existing) accounts that the CLI can use. + ## Backups See this [FAQ](/self-hosting/faq/backup). diff --git a/docs/docs/self-hosting/guides/configuring-s3.md b/docs/docs/self-hosting/guides/configuring-s3.md index 78f28d92ae..55f2f3b356 100644 --- a/docs/docs/self-hosting/guides/configuring-s3.md +++ b/docs/docs/self-hosting/guides/configuring-s3.md @@ -29,7 +29,7 @@ A file upload flows as follows: The upshot of this is that _both_ the client and museum should be able to reach your S3 bucket. -## Configuring S3 +## Configuring S3 The URL for the S3 bucket is configured in [scripts/compose/credentials.yaml](https://github.com/ente-io/ente/blob/main/server/scripts/compose/credentials.yaml#L10). @@ -38,9 +38,8 @@ You can edit this file directly while testing, though it is more robust to create a `museum.yaml` (in the same folder as the Docker compose file) and to setup your custom configuration there. -> [!TIP] -> For more details about these configuration objects, see the documentation for -> the `s3` object in +> [!TIP] For more details about these configuration objects, see the +> documentation for the `s3` object in > [configurations/local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml). By default, you only need to configure the endpoint for the first bucket. @@ -56,13 +55,14 @@ components of the setup to communicate with each other seamlessly. The same principle applies if you're deploying to your custom domain. -## Replication +## Replication ![Replication](/replication.png) +

Community contributed diagram of Ente's replication process

> [!IMPORTANT] -> +> > As of now, replication works only if all the 3 storage type needs are > fulfilled (1 hot, 1 cold and 1 glacier storage). > @@ -72,10 +72,10 @@ If you're wondering why there are 3 buckets on the MinIO UI - that's because our production instance uses these to perform [replication](https://ente.io/reliability/). -If you're also wondering about why the bucket names are specifically what they are, -it's because that is exactly what we are using on our production instance. -We use `b2-eu-cen` as hot, `wasabi-eu-central-2-v3` as cold (also the secondary hot) -and `scw-eu-fr-v3` as glacier storage. As of now, all of this is hardcoded. +If you're also wondering about why the bucket names are specifically what they +are, it's because that is exactly what we are using on our production instance. +We use `b2-eu-cen` as hot, `wasabi-eu-central-2-v3` as cold (also the secondary +hot) and `scw-eu-fr-v3` as glacier storage. As of now, all of this is hardcoded. Hence, the same hardcoded configuration is applied when you self host Ente. In a self hosted Ente instance replication is turned off by default. When @@ -84,16 +84,15 @@ other two are ignored. Only the names here are specifically fixed, but in the configuration body you can put any other keys. It does not have any relation with `b2`, `wasabi` or even `scaleway`. -Use the `s3.hot_storage.primary` option if you'd like to set one of the other +Use the `s3.hot_storage.primary` option if you'd like to set one of the other predefined buckets as the primary bucket. -## SSL Configuration +## SSL Configuration > [!NOTE] > > If you need to configure SSL, you'll need to turn off `s3.are_local_buckets` > (which disables SSL in the default starter compose template). -> Disabling `s3.are_local_buckets` also switches to the subdomain style URLs for the buckets. However, not all S3 providers support these. In particular, MinIO @@ -122,45 +121,3 @@ s3: region: eu-central-2 bucket: b2-eu-cen ``` - -## Frequently encountered errors - -Here are some errors our community members frequently encountered with the -context and potential fixes. - -In most situations, the problem is because of a minor mistakes or -misconfiguration. Please make sure to `reverse_proxy` museum to a domain and -check your S3 credentials and whole configuration file for any minor -misconfigurations. - -It is also suggested that the user setups bucket CORS on MinIO or any external -S3 service provider they are connecting to. To setup bucket CORS, please [read -this](/self-hosting/guides/external-s3#_5-fix-potential-cors-issue-with-your-bucket). - -### 403 Forbidden - -If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but -uploads are still failing, it could be a credentials or permissions issue. - -A telltale sign of this is that in the museum logs you can see `403 Forbidden` -errors about it not able to find the size of a file even though the -corresponding object exists in the S3 bucket. - -This could be because - -1. The bucket CORS rules do not allow museum to access these objects. For - uploading files from the browser, you will need to set `allowedOrigins` to - `*`, and allow the `X-Auth-Token`, `X-Client-Package` headers configuration - too. [Here is an example of a working - configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204). - -2. The credentials are not being picked up (you might be setting the correct - credentials, but not in the place where museum reads them from). - -### Mismatch in file size - -The "Mismatch in file size" error mostly occurs in a situation where the client -(`1`) is re-uploading a file which is already in the bucket with a different -file size. The reason for re-upload could be anything including network issue, -sudden killing of app before the upload is complete and etc. - diff --git a/docs/docs/self-hosting/guides/custom-server/index.md b/docs/docs/self-hosting/guides/custom-server/index.md index 86060ba909..63ba371eb8 100644 --- a/docs/docs/self-hosting/guides/custom-server/index.md +++ b/docs/docs/self-hosting/guides/custom-server/index.md @@ -111,5 +111,5 @@ network, you need to use the public IP or hostname. > [!TIP] > > If you're having trouble uploading from your mobile app, it is likely that -> museum is not able to connect to your S3 storage. See the [Configuring -> S3](/self-hosting/guides/configuring-s3) guide for more details. +> museum is not able to connect to your S3 storage. See the +> [Configuring S3](/self-hosting/guides/configuring-s3) guide for more details. diff --git a/docs/docs/self-hosting/guides/external-s3.md b/docs/docs/self-hosting/guides/external-s3.md index 46b66769ce..0a919092c0 100644 --- a/docs/docs/self-hosting/guides/external-s3.md +++ b/docs/docs/self-hosting/guides/external-s3.md @@ -250,64 +250,6 @@ docker compose exec -i postgres psql -U pguser -d ente_db -c "INSERT INTO storag After few reloads, you should see 1 To of quota. -## 5. Fix potential CORS issue with your bucket - -### For AWS S3 - -If you cannot upload a photo due to a CORS issue, you need to fix the CORS -configuration of your bucket. - -Create a `cors.json` file with the following content: - -```json -{ - "CORSRules": [ - { - "AllowedOrigins": ["*"], - "AllowedHeaders": ["*"], - "AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"], - "MaxAgeSeconds": 3000, - "ExposeHeaders": ["Etag"] - } - ] -} -``` - -You may want to change the `AllowedOrigins` to a more restrictive value. - -If you are using AWS for S3, you can execute the below command to get rid of -CORS. Make sure to enter the right path for the `cors.json` file. - -```bash -aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/cors.json -``` - -### For Self-hosted Minio Instance - -> Important: MinIO does not take JSON CORS file as the input, instead you will -> have to build a CORS.xml file or just convert the above `cors.json` to XML. - -A minor requirement here is the tool `mc` for managing buckets via command line -interface. Checkout the `mc set alias` document to configure alias for your -instance and bucket. After this you will be prompted for your AccessKey and -Secret, which is your username and password, go ahead and enter that. - -```sh -mc cors set // api cors_allow_origin="*" -``` - -You can create also `.csv` file and dump the list of origins you would like to -allow and replace the `*` with `path` to the CSV file. - -Now, uploads should be working fine. - ## Related Some other users have also shared their setups. @@ -315,3 +257,5 @@ Some other users have also shared their setups. - [Using Traefik](https://github.com/ente-io/ente/pull/3663) - [Building custom images from source (Linux)](https://github.com/ente-io/ente/discussions/3778) + +- [Troubleshooting Bucket CORS](/self-hosting/troubleshooting/bucket-cors) diff --git a/docs/docs/self-hosting/guides/from-source.md b/docs/docs/self-hosting/guides/from-source.md index df3ec7e5fb..9d010190dc 100644 --- a/docs/docs/self-hosting/guides/from-source.md +++ b/docs/docs/self-hosting/guides/from-source.md @@ -3,13 +3,12 @@ title: Ente from Source description: Getting started self hosting Ente Photos and/or Ente Auth --- - # Ente from Source -> [!WARNING] NOTE -> The below documentation will cover instructions about self-hosting the web app manually. If you -> want to deploy Ente hassle free, use the [one line](https://ente.io/blog/self-hosting-quickstart/) -> command to setup Ente. This guide might be deprecated in the near future. +> [!WARNING] NOTE The below documentation will cover instructions about +> self-hosting the web app manually. If you want to deploy Ente hassle free, use +> the [one line](https://ente.io/blog/self-hosting-quickstart/) command to setup +> Ente. This guide might be deprecated in the near future. ## Installing Docker @@ -63,8 +62,9 @@ apps and configure them to use your ## Web app with Docker and Compose -The instructoins in previous section were just a temporary way to run the web app locally. -To run the web apps as services, the user has to build a docker image manually. +The instructoins in previous section were just a temporary way to run the web +app locally. To run the web apps as services, the user has to build a docker +image manually. > [!IMPORTANT] > @@ -144,7 +144,7 @@ docker build -t : --no-cache --progress plain . You can always edit the Dockerfile and remove the steps for apps which you do not intend to install on your system (like auth or cast) and opt out of those. -Regarding Albums App, take a note that they are not apps with navigable pages, +Regarding Albums App, take a note that they are not apps with navigable pages, if accessed on the web-browser they will simply redirect to ente.web.io. ## compose.yaml @@ -175,17 +175,17 @@ docker compose up -d # --build docker compose logs ``` -## Configure App Endpoints +## Configure App Endpoints -> [!NOTE] -> Previously, this was dependent on the env variables `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT` -> and etc. Please check the below documentation to update your setup configurations +> [!NOTE] Previously, this was dependent on the env variables +> `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT` and etc. Please check the below +> documentation to update your setup configurations -You can configure the web endpoints for the other apps including Accounts, Albums -Family and Cast in your `museum.yaml` configuration file. Checkout +You can configure the web endpoints for the other apps including Accounts, +Albums Family and Cast in your `museum.yaml` configuration file. Checkout [`local.yaml`](https://github.com/ente-io/ente/blob/543411254b2bb55bd00a0e515dcafa12d12d3b35/server/configurations/local.yaml#L76-L89) -to configure the endpoints. Make sure to setup up your DNS Records accordingly to the -similar URL's you set up in `museum.yaml`. +to configure the endpoints. Make sure to setup up your DNS Records accordingly +to the similar URL's you set up in `museum.yaml`. Next part is to configure the web server. @@ -197,7 +197,7 @@ ports). The web server of choice in this guide is [Caddy](https://caddyserver.com) because with caddy you don't have to manually configure/setup SSL ceritifcates as caddy will take care of that. -```sh +```groovy photos.yourdomain.com { reverse_proxy http://localhost:3001 # for logging @@ -219,6 +219,7 @@ Next, start the caddy server :). sudo systemctl enable caddy sudo systemctl daemon-reload + sudo systemctl start caddy ``` diff --git a/docs/docs/self-hosting/guides/Tailscale.md b/docs/docs/self-hosting/guides/tailscale.md similarity index 60% rename from docs/docs/self-hosting/guides/Tailscale.md rename to docs/docs/self-hosting/guides/tailscale.md index 1f0a7593ed..20983e8e7b 100644 --- a/docs/docs/self-hosting/guides/Tailscale.md +++ b/docs/docs/self-hosting/guides/tailscale.md @@ -2,39 +2,56 @@ title: Self Hosting with Tailscale (Community) description: Guides for self-hosting Ente Photos and/or Ente Auth with Tailscale --- + # Guide -This guide aims to achieve self-hosting Ente photos or Ente-Auth with tailscale (TSDPROXY) without exposing any port OR if someone is behind CGNAT and cannot open any port on the internet but want to run their own selfhosted service for themselves, friends and family only. +This guide aims to achieve self-hosting Ente photos or Ente-Auth with tailscale +(TSDPROXY) without exposing any port OR if someone is behind CGNAT and cannot +open any port on the internet but want to run their own selfhosted service for +themselves, friends and family only. Before getting start keep the following NOTE in mind. -> [!NOTE] -> If someone is behind double or triple CGNAT; must install tailscale system wide by running `curl -fsSL https://tailscale.com/install.sh | sh` in your linux terminal and `sudo tailscale up` otherwise dns resolver will fail and uploading will not work. This is not necessary for those who are not behing CGNAT. -> This guide also work on docker rootless and normal. +> [!NOTE] If someone is behind double or triple CGNAT; must install tailscale +> system wide by running `curl -fsSL https://tailscale.com/install.sh | sh` in +> your linux terminal and `sudo tailscale up` otherwise dns resolver will fail +> and uploading will not work. This is not necessary for those who are not +> behing CGNAT. This guide also work on docker rootless and normal. -> [!CAUTION] -Remember that current docker update 28.0.0 has some bug and cannot connect to external network. Make sure to install docker-ce 27.5.0, docker-ce-rootless-extras 27.5.0 and docker-ce-cli 27.5.0. Hopefully docker 28.1.0 will resolve this issue in next week. Refrence links are [Moby Github Repo Issues 49511](https://github.com/moby/moby/issues/49511) and [Moby Github Repo Issues 49519](https://github.com/moby/moby/issues/49519) - -> [!IMPORTANT] -> For Docker rootless, the user must have local permissions for all directories required by the Ente-photos self-hosted server. This can be achieved by running `sudo chown -R 1000:1000 /home/ubuntu/docker/ente`. In the Linux terminal, you can check the UID with `id -u` or simply `id`. The first user typically has UID 1000. -> To allow listening and pinging on any port without root privileges, create a file called `/etc/sysctl.d/99-rootless.conf` with the following content: +> [!IMPORTANT] For Docker rootless, the user must have local permissions for all +> directories required by the Ente-photos self-hosted server. This can be +> achieved by running `sudo chown -R 1000:1000 /home/ubuntu/docker/ente`. In the +> Linux terminal, you can check the UID with `id -u` or simply `id`. The first +> user typically has UID 1000. To allow listening and pinging on any port +> without root privileges, create a file called `/etc/sysctl.d/99-rootless.conf` +> with the following content: +> > ``` > net.ipv4.ip_unprivileged_port_start=0 > net.ipv4.ping_group_range = 0 2147483647 > ``` -> than run `sudo sysctl --system`. -> Create `~/.config/systemd/user/docker.service.d/override.conf` with the following content: +> +> than run `sudo sysctl --system`. Create +> `~/.config/systemd/user/docker.service.d/override.conf` with the following +> content: +> > ``` > [Service] > Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns" > Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" > ``` -> and Restart the docker daemon -> `systemctl --user restart docker` -> Instead of `--volume /var/run/docker.sock:/var/run/docker.sock` in TSDPROXY compose.yaml, use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock` +> +> and Restart the docker daemon `systemctl --user restart docker` Instead of +> `--volume /var/run/docker.sock:/var/run/docker.sock` in TSDPROXY compose.yaml, +> use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock` + +## GETTING START WITH SETUP + +First of all create a directory +`sudo mkdir -p /home/ubuntu/docker/tsdproxy/config` than `cd docker/tsdproxy` +and create compose.yaml file by running `sudo nano compose.yaml`. Populate it +with the following: -## GETTING START WITH SETUP ## -First of all create a directory `sudo mkdir -p /home/ubuntu/docker/tsdproxy/config` than `cd docker/tsdproxy` and create compose.yaml file by running `sudo nano compose.yaml`. Populate it with the following: ``` services: tsdproxy: @@ -62,9 +79,18 @@ networks: proxy: name: proxy ``` -Now login into your tailscale account admin counsle > settings > keys > Generate authkey. Give any description and must select resuable, because the key get purged if not selected after rebooting machine. It is advisable to create **Tags** in **ACLs settings** `tag: tsdproxy` `tag: ente` `tag: minio` as well. This will create a tag nodes with no key expirory. One is safe to reboot restart docker or machine. -> Copy the generated authkey as it is shown only once. -Make tsdproxy.yaml file in `cd docker/tsdproxy/config` by running `sudo nano tsdproxy.yaml` and pupolate it with the following contant: + +Now login into your tailscale account admin counsle > settings > keys > Generate +authkey. Give any description and must select resuable, because the key get +purged if not selected after rebooting machine. It is advisable to create +**Tags** in **ACLs settings** `tag: tsdproxy` `tag: ente` `tag: minio` as well. +This will create a tag nodes with no key expirory. One is safe to reboot restart +docker or machine. + +> Copy the generated authkey as it is shown only once. Make tsdproxy.yaml file +> in `cd docker/tsdproxy/config` by running `sudo nano tsdproxy.yaml` and +> pupolate it with the following contant: + ``` defaultproxyprovider: default docker: @@ -87,12 +113,20 @@ log: json: false proxyaccesslog: true ``` -In the same directory run `sudo nano authkey` and paste the authkey just copied earlier from tailscale admin counsel. -> Here Tailscale (TSDPROXY) setup is complet in all respect. Just run `docker compose up -d`. Check your tailscale amdin counsel and you will see tsdproxy node up and running. Make sure that **HTTPS** is enabled in tailscale DNS settings. -> You can visit the TSDPROXY web GUI by https://tsdproxy.xyz.ts.net. (xyz is change value for everyone) -## ente Part ## +In the same directory run `sudo nano authkey` and paste the authkey just copied +earlier from tailscale admin counsel. + +> Here Tailscale (TSDPROXY) setup is complet in all respect. Just run +> `docker compose up -d`. Check your tailscale amdin counsel and you will see +> tsdproxy node up and running. Make sure that **HTTPS** is enabled in tailscale +> DNS settings. You can visit the TSDPROXY web GUI by +> https://tsdproxy.xyz.ts.net. (xyz is change value for everyone) + +## ente Part + First make the following necessary files/directories: + ``` sudo mkdir -p /home/ubuntu/docker/ente/custom-logs sudo mkdir -p /home/ubuntu/docker/ente/data @@ -100,9 +134,14 @@ sudo mkdir -p /home/ubuntu/docker/ente/minio-data sudo mkdir -p /home/ubuntu/docker/ente/postgres-data sudo mkdir -p /home/ubuntu/docker/ente/scripts/compose ``` -Than give user permission for each of the above directory. `sudo chown -R 1000:1000 /home/ubuntu/docker/ente/custom-logs` etc etc. Make sure not to skip `/home/ubuntu/docker/tsdproxy/config` -`cd docker/ente/script/compose` and run `sudo nano credentials.yaml` than populate it with the following: +Than give user permission for each of the above directory. +`sudo chown -R 1000:1000 /home/ubuntu/docker/ente/custom-logs` etc etc. Make +sure not to skip `/home/ubuntu/docker/tsdproxy/config` + +`cd docker/ente/script/compose` and run `sudo nano credentials.yaml` than +populate it with the following: + ``` db: host: postgres @@ -134,7 +173,9 @@ s3: bucket: scw-eu-fr-v3 ``` -In the same directory run `sudo nano minio-provision.sh` and populate it with the following contant: +In the same directory run `sudo nano minio-provision.sh` and populate it with +the following contant: + ``` #!/bin/sh @@ -154,7 +195,9 @@ mc mb -p wasabi-eu-central-2-v3 mc mb -p scw-eu-fr-v3 ``` -Now `cd docker/ente` and run `sudo nano docker-compose.yaml` and populate it with the following: +Now `cd docker/ente` and run `sudo nano docker-compose.yaml` and populate it +with the following: + ``` services: museum: @@ -255,32 +298,52 @@ services: networks: ente: name: ente - + proxy: external: true ``` -> Thats it. Run `docker compose up -d`. Wait till every container become healthy. Open web browser. Make sure tailscale is installed on the machine. Visit https://ente.xyz.ts.net/ping. It will pong. All good if you see it. First time it will take minute or two to get SSL cert. Downnload Desktop or mobile app. Tap 7 time on the screen, which will prompt developer mode. Add https://ente.xyz.ts.net. Add new user. When asked for OTP. Just go to linux terminal and run `docker logs ente-museum-1`. Search for userauth. Feed the six digit and Done. +> Thats it. Run `docker compose up -d`. Wait till every container become +> healthy. Open web browser. Make sure tailscale is installed on the machine. +> Visit https://ente.xyz.ts.net/ping. It will pong. All good if you see it. +> First time it will take minute or two to get SSL cert. Downnload Desktop or +> mobile app. Tap 7 time on the screen, which will prompt developer mode. Add +> https://ente.xyz.ts.net. Add new user. When asked for OTP. Just go to linux +> terminal and run `docker logs ente-museum-1`. Search for userauth. Feed the +> six digit and Done. + +> For getting 100TB (limitless) storage. Just Install ente-cli for windows. +> Extract it and add folder. Name it **export**. Add config.yaml file along and +> populate it with the following: -> For getting 100TB (limitless) storage. Just Install ente-cli for windows. Extract it and add folder. Name it **export**. Add config.yaml file along and populate it with the following: ``` endpoint: api: "https://ente.xyz.ts.net" accounts: "http://localhost:3001" - + log: false ``` -Right-Click in the directory where you have extracted ente-cli. Select `open in terminal`. Run + +Right-Click in the directory where you have extracted ente-cli. Select +`open in terminal`. Run + ``` .\ente.exe account bob # change bob to yours ``` -Hit Enter twice. -For export directory, just write export. As already created **export** folder earlier. -**Write email. The one which is already used befor when creating ente account in ente desktop app.** -Type the same Password used before for the account.Run + +Hit Enter twice. For export directory, just write export. As already created +**export** folder earlier. **Write email. The one which is already used befor +when creating ente account in ente desktop app.** Type the same Password used +before for the account.Run + ``` .\ente.ext account list ``` + This will list all account details. Copy Acount ID. -> Navigate to museum.yaml file. `cd docker/ente`. Run `sudo nano museum.yaml` and add the account ID under Admins. Delete any previous entries. -Restart ente-museum-1 container from linux terminal. Run `docker restart ente-museum-1`. All well, now you will have 100TB storage. Repeat if for any other accounts you want to give unlimited storage access. + +> Navigate to museum.yaml file. `cd docker/ente`. Run `sudo nano museum.yaml` +> and add the account ID under Admins. Delete any previous entries. Restart +> ente-museum-1 container from linux terminal. Run +> `docker restart ente-museum-1`. All well, now you will have 100TB storage. +> Repeat if for any other accounts you want to give unlimited storage access. diff --git a/docs/docs/self-hosting/guides/web-app.md b/docs/docs/self-hosting/guides/web-app.md index 015cb7d986..a3061e3004 100644 --- a/docs/docs/self-hosting/guides/web-app.md +++ b/docs/docs/self-hosting/guides/web-app.md @@ -5,22 +5,20 @@ description: server --- - -> [!WARNING] NOTE -> This page covers documentation around self-hosting the web app manually. If you -> want to deploy Ente hassle free, please use the [one line](https://ente.io/blog/self-hosting-quickstart/) -> command to setup Ente. This guide might be deprecated in the near future. +> [!WARNING] NOTE This page covers documentation around self-hosting the web app +> manually. If you want to deploy Ente hassle free, please use the +> [one line](https://ente.io/blog/self-hosting-quickstart/) command to setup +> Ente. This guide might be deprecated in the near future. # Web app The getting started instructions mention using `yarn dev` (which is an alias of `yarn dev:photos`) to serve your web app. ->[!IMPORTANT] -> Please note that Ente's Web App supports the Yarn version 1.22.xx or 1.22.22 specifically. -> Make sure to install the right version or modify your yarn installation to meet the requirements. -> The user might end up into unknown version and dependency related errors if yarn -> is on different version. +> [!IMPORTANT] Please note that Ente's Web App supports the Yarn version 1.22.xx +> or 1.22.22 specifically. Make sure to install the right version or modify your +> yarn installation to meet the requirements. The user might end up into unknown +> version and dependency related errors if yarn is on different version. ```sh cd ente/web @@ -146,15 +144,15 @@ docker compose logs ## Configure App Endpoints -> [!NOTE] -> Previously, this was dependent on the env variables `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT` -> and etc. Please check the below documentation to update your setup configurations +> [!NOTE] Previously, this was dependent on the env variables +> `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT` and etc. Please check the below +> documentation to update your setup configurations -You can configure the web endpoints for the other apps including Accounts, Albums -Family and Cast in your `museum.yaml` configuration file. Checkout +You can configure the web endpoints for the other apps including Accounts, +Albums Family and Cast in your `museum.yaml` configuration file. Checkout [`local.yaml`](https://github.com/ente-io/ente/blob/543411254b2bb55bd00a0e515dcafa12d12d3b35/server/configurations/local.yaml#L76-L89) -to configure the endpoints. Make sure to setup up your DNS Records accordingly to the -similar URL's you set up in `museum.yaml`. +to configure the endpoints. Make sure to setup up your DNS Records accordingly +to the similar URL's you set up in `museum.yaml`. Next part is to configure the web server. diff --git a/docs/docs/self-hosting/index.md b/docs/docs/self-hosting/index.md index c0eef22f2c..79afd831ba 100644 --- a/docs/docs/self-hosting/index.md +++ b/docs/docs/self-hosting/index.md @@ -5,20 +5,32 @@ description: Getting started self hosting Ente Photos and/or Ente Auth # Self Hosting -The entire source code for Ente is open source, including the servers. This is +The entire source code for Ente is open source, +[including the servers](https://ente.io/blog/open-sourcing-our-server/). This is the same code we use for our own cloud service. -> [!TIP] -> -> You might find our [blog post](https://ente.io/blog/open-sourcing-our-server/) -> announcing the open sourcing of our server useful. +## Requirements -## System requirements +### Hardware -The server has minimal resource requirements, running as a lightweight Go -binary. It performs well on small cloud instances, old laptops, and even +The server is capable of running on minimal resource requirements as a +lightweight Go binary, since most of the intensive computational tasks are done +on the client. It performs well on small cloud instances, old laptops, and even [low-end embedded devices](https://github.com/ente-io/ente/discussions/594). +### Software + +#### Operating System + +Any Linux or \*nix operating system, Ubuntu or Debian is recommended to have a +good Docker experience. Non-Linux operating systems tend to provide poor +experience with Docker and difficulty with troubleshooting and assistance. + +#### Docker + +Required for running Ente's server, web application and dependent services +(database and object storage) + ## Getting started Run this command on your terminal to setup Ente. @@ -28,7 +40,8 @@ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ente-io/ente/main/server/q ``` The above `curl` command pulls the Docker image, creates a directory `my-ente` -in the current working directory and starts all containers required to run Ente. +in the current working directory and prompts to start the cluster, which upon +entering `y`, starts all the containers required to run Ente. ![quickstart](/quickstart.png) diff --git a/docs/docs/self-hosting/museum.md b/docs/docs/self-hosting/museum.md index 4d1da34b0a..cb5263104f 100644 --- a/docs/docs/self-hosting/museum.md +++ b/docs/docs/self-hosting/museum.md @@ -16,10 +16,10 @@ If you used our quickstart script, your `my-ente` directory will include a PostgreSQL and MinIO. > [!TIP] -> +> > Always do `docker compose down` inside your `my-ente` directory. If you've -> made changes to `museum.yaml`, restart the containers with `docker compose up -> -d ` to see your changes in action. +> made changes to `museum.yaml`, restart the containers with +> `docker compose up -d ` to see your changes in action. ## S3 buckets @@ -33,37 +33,45 @@ Check out [Configuring S3](/self-hosting/guides/configuring-s3.md) to understand more about configuring S3 buckets. MinIO uses the port `3200` for API Endpoints and their web app runs over -`:3201`. You can login to MinIO Web Console by opening `localhost:3201` in your browser. +`:3201`. You can login to MinIO Web Console by opening `localhost:3201` in your +browser. -If you face any issues related to uploads then checkout [Troubleshooting bucket -CORS](/self-hosting/troubleshooting/bucket-cors) and [Frequently encountered S3 -errors](/self-hosting/guides/configuring-s3#frequently-encountered-errors). +If you face any issues related to uploads then checkout +[Troubleshooting bucket CORS](/self-hosting/troubleshooting/bucket-cors) and +[Frequently encountered S3 errors](/self-hosting/guides/configuring-s3#frequently-encountered-errors). ## Web apps The web apps for Ente Photos is divided into multiple sub-apps like albums, -cast, auth, etc. These endpoints are configurable in the museum.yaml under the +cast, auth, etc. These endpoints are configurable in `museum.yaml` under the `apps.*` section. -For example, +For example, ```yaml apps: - public-albums: albums.myente.xyz - cast: cast.myente.xyz - accounts: accounts.myente.xyz - family: family.myente.xyz + public-albums: https://albums.myente.xyz + cast: https://cast.myente.xyz + accounts: https://accounts.myente.xyz + family: https://family.myente.xyz ``` -By default, all the values redirect to our publicly hosted production services. +> [!IMPORTANT] By default, all the values redirect to our publicly hosted +> production services. For example, if `public-albums` is not configured your +> shared album will use the `albums.ente.io` URL. + After you are done with filling the values, restart museum and the app will start utilizing those endpoints instead of Ente's production instances. Once you have configured all the necessary endpoints, `cd` into `my-ente` and stop all the Docker containers with `docker compose down` and restart them with -`docker compose up -d`. +`docker compose up -d`. Similarly, you can use the default [`local.yaml`](https://github.com/ente-io/ente/tree/main/server/configurations/local.yaml) as a reference for building a functioning `museum.yaml` for many other functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc. + +## References + +- [Environment variables and ports](/self-hosting/faq/environment) diff --git a/docs/docs/self-hosting/reverse-proxy.md b/docs/docs/self-hosting/reverse-proxy.md index 7ef79ac412..97ef7da570 100644 --- a/docs/docs/self-hosting/reverse-proxy.md +++ b/docs/docs/self-hosting/reverse-proxy.md @@ -22,28 +22,32 @@ server on your machine. Setting up a reverse proxy with Caddy is easy and straightforward. -Firstly, install Caddy on your server. +Firstly, install Caddy on your server. ```sh sudo apt install caddy -``` +``` After the installation is complete, a `Caddyfile` is created on the path `/etc/caddy/`. This file is used to configure reverse proxies among other things. -```yaml +```groovy # Caddyfile - myente.xyz is just an example. + api.myente.xyz { reverse_proxy http://localhost:8080 } + ente.myente.xyz { reverse_proxy http://localhost:3000 } + #...and so on for other endpoints ``` -After a hard-reload, the Ente Photos web app should be up on https://ente.myente.xyz. +After a hard-reload, the Ente Photos web app should be up on +https://ente.myente.xyz. If you are using a different tool for reverse proxy (like nginx), please check out their documentation. diff --git a/docs/docs/self-hosting/troubleshooting/bucket-cors.md b/docs/docs/self-hosting/troubleshooting/bucket-cors.md index 19c1dbff47..8bab1f7012 100644 --- a/docs/docs/self-hosting/troubleshooting/bucket-cors.md +++ b/docs/docs/self-hosting/troubleshooting/bucket-cors.md @@ -37,13 +37,21 @@ aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/ ## For Self-hosted Minio Instance -> Important: MinIO does not take JSON CORS file as the input, instead you will -> have to build a CORS.xml file or just convert the above `cors.json` to XML. +::: warning + +- MinIO does not support bucket CORS in the community edition which is used by + default. For more information, check + [this discussion](https://github.com/minio/minio/discussions/20841). However, + global CORS configuration is possible. +- MinIO does not take JSON CORS file as the input, instead you will have to + build a CORS.xml file or just convert the above `cors.json` to XML. + +::: A minor requirement here is the tool `mc` for managing buckets via command line interface. Checkout the `mc set alias` document to configure alias for your instance and bucket. After this you will be prompted for your AccessKey and -Secret, which is your username and password, go ahead and enter that. +Secret, which is your username and password. ```sh mc cors set // api cors_allow_origin="*" You can create also `.csv` file and dump the list of origins you would like to allow and replace the `*` with `path` to the CSV file. -Now, uploads should be working fine. \ No newline at end of file +Now, uploads should be working fine. diff --git a/docs/docs/self-hosting/troubleshooting/docker.md b/docs/docs/self-hosting/troubleshooting/docker.md index 6c80070b34..902e8c9cfd 100644 --- a/docs/docs/self-hosting/troubleshooting/docker.md +++ b/docs/docs/self-hosting/troubleshooting/docker.md @@ -1,5 +1,5 @@ --- -title: Docker errors +title: Docker Errors description: Fixing docker related errors when trying to self host Ente --- @@ -34,30 +34,30 @@ perform the same configuration by removing the "post_start" hook, and adding a new service definition: ```yaml - minio-provision: +minio-provision: image: minio/mc depends_on: - - minio + - minio volumes: - - minio-data:/data + - minio-data:/data networks: - - internal + - internal entrypoint: | - sh -c ' - #!/bin/sh + sh -c ' + #!/bin/sh - while ! mc config host add h0 http://minio:3200 changeme changeme1234 - do - echo "waiting for minio..." - sleep 0.5 - done + while ! mc config host add h0 http://minio:3200 changeme changeme1234 + do + echo "waiting for minio..." + sleep 0.5 + done - cd /data + cd /data - mc mb -p b2-eu-cen - mc mb -p wasabi-eu-central-2-v3 - mc mb -p scw-eu-fr-v3 - ' + mc mb -p b2-eu-cen + mc mb -p wasabi-eu-central-2-v3 + mc mb -p scw-eu-fr-v3 + ' ``` ## start_interval @@ -114,7 +114,7 @@ volumes. If you're sure of what you're doing, the volumes can be deleted by -``` +```sh docker volume ls ``` @@ -124,6 +124,13 @@ to list them, and then delete the ones that begin with `my-ente` using that'll delete all volumes (Ente or otherwise) on your machine that are not currently in use by a running docker container. +An alternative way is to delete the volumes along with removal of cluster's +containers using `docker compose` inside `my-ente` directory. + +```sh +docker compose down --volumes +``` + If you're unsure about removing volumes, another alternative is to rename your `my-ente` folder. Docker uses the folder name to determine the volume name prefix, so giving it a different name will cause Docker to create a volume diff --git a/docs/docs/self-hosting/troubleshooting/keyring.md b/docs/docs/self-hosting/troubleshooting/keyring.md index 399595ba3f..56a5807fa5 100644 --- a/docs/docs/self-hosting/troubleshooting/keyring.md +++ b/docs/docs/self-hosting/troubleshooting/keyring.md @@ -5,8 +5,8 @@ description: A quick hotfix for keyring errors while running Ente CLI. # Ente CLI Secrets -Ente CLI makes use of keyring for storing sensitive information like your -passwords. And running the cli straight out of the box might give you some +Ente CLI makes use of system keyring for storing sensitive information like your +passwords. And running the CLI straight out of the box might give you some errors related to keyrings in some case. Follow the below steps to run Ente CLI and also avoid keyrings errors. diff --git a/docs/docs/self-hosting/troubleshooting/misc.md b/docs/docs/self-hosting/troubleshooting/misc.md index 65c1e7ffd1..ff6ceda762 100644 --- a/docs/docs/self-hosting/troubleshooting/misc.md +++ b/docs/docs/self-hosting/troubleshooting/misc.md @@ -3,14 +3,14 @@ title: General troubleshooting cases description: Fixing various errors when trying to self host Ente --- -## Functionality not working on self hosted +## Functionality not working on self hosted instance If some specific functionality (e.g. album listing, video playback) does not work on your self hosted instance, it is possible that you have set _some_, but not _all_ needed CSP headers (by default, CSP is not enabled). To expand on it - by default, currently the generated build does not enable CSP -headers. The generated build includes a _headers file that Cloudflare will use +headers. The generated build includes a \_headers file that Cloudflare will use to set HTTP response headers, but even these do not enable CSP, it is set to a report only mode. @@ -18,7 +18,7 @@ However, your web server might be setting some CSP policy. If so, then you will need to ensure that all necessary CSP headers are set. You can see the current -[_headers](https://github.com/ente-io/ente/blob/main/web/apps/photos/public/_headers) +[\_headers](https://github.com/ente-io/ente/blob/main/web/apps/photos/public/_headers) file contents to use a template for your CSP policy. The `Content-Security-Policy-Report-Only` value will show you the CSP headers in "dry run" report-only mode we're setting - you can use that as a template, @@ -28,8 +28,8 @@ How do you know if this is the problem you're facing? The browser console _might_ be giving you errors when you try to open the page and perform the corresponding function. -> Refused to load https://subdomain.example.org/... because it does not appear -> in the script-src directive of the Content Security Policy. +> Refused to load https://subdomain.example.org/... because it does not appear +> in the script-src directive of the Content Security Policy. This is not guaranteed, each browsers handles CSP errors differently, and some may silently swallow it. diff --git a/docs/docs/self-hosting/troubleshooting/uploads.md b/docs/docs/self-hosting/troubleshooting/uploads.md index 435a5e93c6..6cad97f201 100644 --- a/docs/docs/self-hosting/troubleshooting/uploads.md +++ b/docs/docs/self-hosting/troubleshooting/uploads.md @@ -1,13 +1,55 @@ --- -title: Uploads failing +title: Uploads description: Fixing upload errors when trying to self host Ente --- -# Uploads failing +# Troubleshooting upload failures -If uploads to your minio are failing, you need to ensure that you've configured -the S3 bucket `endpoint` in `credentials.yaml` (or `museum.yaml`) to, say, -`yourserverip:3200`. This can be any host or port, it just need to be a value -that is reachable from both your client and from museum. +Here are some errors our community members frequently encountered with the +context and potential fixes. -For more details, see [configuring-s3](/self-hosting/guides/configuring-s3). +Fundamentally in most situations, the problem is because of minor mistakes or +misconfiguration. Please make sure to reverse proxy museum and MinIO API +endpoint to a domain and check your S3 credentials and whole configuration file +for any minor misconfigurations. + +It is also suggested that the user setups bucket CORS or global CORS on MinIO or +any external S3 service provider they are connecting to. To setup bucket CORS, +please [read this](/self-hosting/troubleshooting/bucket-cors). + +## What is S3 and how is it incorporated in Ente ? + +S3 is an cloud storage protocol made by Amazon (specifically AWS). S3 is +designed to store files and data as objects inside buckets and it is mostly used +for online backups and storing different types of files. + +Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 +provider. MinIO supports the Amazon S3 protocol and leverages your disk storage +to dump all the uploaded files as encrypted object blobs. + +## 403 Forbidden + +If museum is able to make a network connection to your S3 bucket but uploads are +still failing, it could be a credentials or permissions issue. + +A telltale sign of this is that in the museum logs you can see `403 Forbidden` +errors about it not able to find the size of a file even though the +corresponding object exists in the S3 bucket. + +This could be because + +1. The bucket CORS rules do not allow museum to access these objects. For + uploading files from the browser, you will need to set `allowedOrigins` to + `*`, and allow the `X-Auth-Token`, `X-Client-Package`, `X-Client-Version` + headers configuration too. + [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204). + +2. The credentials are not being picked up (you might be setting the correct + credentials, but not in the place where museum reads them from). + +## Mismatch in file size + +The "Mismatch in file size" error mostly occurs in a situation where the client +is re-uploading a file which is already in the bucket with a different file +size. The reason for re-upload could be anything including network issue, sudden +killing of app before the upload is complete and etc. diff --git a/docs/docs/self-hosting/troubleshooting/yarn.md b/docs/docs/self-hosting/troubleshooting/yarn.md index b4205beb0e..4cc62c405b 100644 --- a/docs/docs/self-hosting/troubleshooting/yarn.md +++ b/docs/docs/self-hosting/troubleshooting/yarn.md @@ -5,8 +5,8 @@ description: Fixing yarn install errors when trying to self host Ente # Yarn -If your `yarn install` is failing, make sure you are using Yarn v1 (also known -as "Yarn Classic"): +If `yarn install` is failing, make sure you are using Yarn v1 (also known as +"Yarn Classic"): - https://classic.yarnpkg.com/lang/en/docs/install diff --git a/docs/package.json b/docs/package.json index bd7635b126..a714aea110 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,7 +5,8 @@ "dev": "vitepress dev docs", "build": "vitepress build docs", "preview": "vitepress preview docs", - "pretty": "prettier --write ." + "pretty": "prettier --write .", + "pretty:check": "prettier -c ." }, "devDependencies": { "prettier": "^3.3.4", diff --git a/infra/copycat-db/src/backup.sh b/infra/copycat-db/src/backup.sh index f197f4b0f0..bbd8a2bb9e 100755 --- a/infra/copycat-db/src/backup.sh +++ b/infra/copycat-db/src/backup.sh @@ -36,7 +36,7 @@ else BACKUP_ID=$(scw rdb backup create instance-id=$SCW_RDB_INSTANCE_ID \ name=$BACKUP_NAME expires-at=$EXPIRY \ database-name=ente_db -o json | jq -r '.id') - scw rdb backup wait $BACKUP_ID timeout=5h + scw rdb backup wait $BACKUP_ID timeout=8h scw rdb backup download output=$BACKUP_FILE \ $(scw rdb backup export $BACKUP_ID --wait -o json | jq -r '.id') fi diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index 2a754f3bd0..a1bf14d6c8 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -10,10 +10,10 @@ import "./App.css"; import FamilyTableComponent from "./components/FamilyComponentTable"; import StorageBonusTableComponent from "./components/StorageBonusTableComponent"; import TokensTableComponent from "./components/TokenTableComponent"; -import type { UserData } from "./components/UserComponent"; import UserComponent from "./components/UserComponent"; import duckieimage from "./components/duckie.png"; import { apiOrigin } from "./services/support"; +import type { UserData, UserResponse } from "./types"; export let email = ""; export let token = ""; @@ -29,38 +29,6 @@ export const setToken = (newToken: string) => { export const getEmail = () => email; export const getToken = () => token; -interface User { - ID: string; - email: string; - creationTime: number; -} - -interface Subscription { - productID: string; - paymentProvider: string; - expiryTime: number; - storage: number; -} - -interface Security { - isEmailMFAEnabled: boolean; - isTwoFactorEnabled: boolean; - passkeys: string; - passkeyCount: number; - canDisableEmailMFA: boolean; -} - -interface UserResponse { - user: User; - subscription: Subscription; - authCodes?: number; - details?: { - usage?: number; - storageBonus?: number; - profileData: Security; - }; -} - const App: React.FC = () => { const [localEmail, setLocalEmail] = useState(""); const [localToken, setLocalToken] = useState(""); @@ -139,7 +107,7 @@ const App: React.FC = () => { console.log("API Response:", userDataResponse); const extractedUserData: UserData = { - User: { + user: { "User ID": userDataResponse.user.ID || "None", Email: userDataResponse.user.email || "None", "Creation time": @@ -147,7 +115,7 @@ const App: React.FC = () => { userDataResponse.user.creationTime / 1000, ).toLocaleString() || "None", }, - Storage: { + storage: { Total: userDataResponse.subscription.storage ? userDataResponse.subscription.storage >= 1024 ** 3 ? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB` @@ -166,7 +134,7 @@ const App: React.FC = () => { : `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB` : "None", }, - Subscription: { + subscription: { "Product ID": userDataResponse.subscription.productID || "None", Provider: @@ -176,7 +144,7 @@ const App: React.FC = () => { userDataResponse.subscription.expiryTime / 1000, ).toLocaleString() || "None", }, - Security: { + security: { "Email MFA": userDataResponse.details?.profileData .isEmailMFAEnabled ? "Enabled" diff --git a/infra/staff/src/components/ChangeEmail.tsx b/infra/staff/src/components/ChangeEmail.tsx index 26407af50e..d3824babc4 100644 --- a/infra/staff/src/components/ChangeEmail.tsx +++ b/infra/staff/src/components/ChangeEmail.tsx @@ -10,10 +10,10 @@ import { import React, { useEffect, useState } from "react"; import { getEmail, getToken } from "../App"; import { apiOrigin } from "../services/support"; -interface ErrorResponse { - message: string; -} +import type { ErrorResponse } from "../types"; +// The below interfaces will only be used in this file +// hence not including them into a sub-merged types file interface ChangeEmailProps { open: boolean; onClose: () => void; diff --git a/infra/staff/src/components/CloseFamily.tsx b/infra/staff/src/components/CloseFamily.tsx index 145c11b505..5ab71bd474 100644 --- a/infra/staff/src/components/CloseFamily.tsx +++ b/infra/staff/src/components/CloseFamily.tsx @@ -10,14 +10,7 @@ import { import React, { useState } from "react"; import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions import { apiOrigin } from "../services/support"; - -interface UserData { - subscription?: { - userID: string; - // Add other properties as per your API response structure - }; - // Add other properties as per your API response structure -} +import type { UserData } from "../types"; interface CloseFamilyProps { open: boolean; diff --git a/infra/staff/src/components/Disable2FA.tsx b/infra/staff/src/components/Disable2FA.tsx index 29a3a6cd5a..62166aef24 100644 --- a/infra/staff/src/components/Disable2FA.tsx +++ b/infra/staff/src/components/Disable2FA.tsx @@ -10,14 +10,7 @@ import { import React, { useState } from "react"; import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions import { apiOrigin } from "../services/support"; - -interface UserData { - subscription?: { - userID: string; - // Add other properties as per your API response structure - }; - // Add other properties as per your API response structure -} +import type { UserData } from "../types"; interface Disable2FAProps { open: boolean; diff --git a/infra/staff/src/components/DisablePasskeys.tsx b/infra/staff/src/components/DisablePasskeys.tsx index 824b2f2f87..a2757a371d 100644 --- a/infra/staff/src/components/DisablePasskeys.tsx +++ b/infra/staff/src/components/DisablePasskeys.tsx @@ -10,20 +10,7 @@ import { import React, { useState } from "react"; import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions import { apiOrigin } from "../services/support"; - -interface UserData { - subscription?: { - userID: string; - // Add other properties as per your API response structure - }; - // Add other properties as per your API response structure -} - -interface DisablePasskeysProps { - open: boolean; - handleClose: () => void; - handleDisablePasskeys: () => void; // Callback to handle disabling passkeys -} +import type { DisablePasskeysProps, UserData } from "../types"; const DisablePasskeys: React.FC = ({ open, diff --git a/infra/staff/src/components/FamilyComponentTable.tsx b/infra/staff/src/components/FamilyComponentTable.tsx index 2171b3aeda..77e7a9ed44 100644 --- a/infra/staff/src/components/FamilyComponentTable.tsx +++ b/infra/staff/src/components/FamilyComponentTable.tsx @@ -13,23 +13,10 @@ import * as React from "react"; import { useEffect, useState } from "react"; import { getEmail, getToken } from "../App"; import { apiOrigin } from "../services/support"; +import type { FamilyMember, UserData } from "../types"; +import { formatUsageToGB } from "../utils/"; import CloseFamily from "./CloseFamily"; -interface FamilyMember { - id: string; - email: string; - status: string; - usage: number; -} - -interface UserData { - details: { - familyData: { - members: FamilyMember[]; - }; - }; -} - const FamilyTableComponent: React.FC = () => { const [familyMembers, setFamilyMembers] = useState([]); const [closeFamilyOpen, setCloseFamilyOpen] = useState(false); @@ -54,7 +41,7 @@ const FamilyTableComponent: React.FC = () => { } const userData = (await response.json()) as UserData; // Typecast to UserData interface const members: FamilyMember[] = - userData.details.familyData.members; + userData.details?.familyData.members ?? []; setFamilyMembers(members); } catch (error) { console.error("Error fetching family data:", error); @@ -69,11 +56,6 @@ const FamilyTableComponent: React.FC = () => { ); }, []); - const formatUsageToGB = (usage: number): string => { - const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2); - return `${usageInGB} GB`; - }; - const handleOpenCloseFamily = () => { setCloseFamilyOpen(true); }; @@ -111,6 +93,9 @@ const FamilyTableComponent: React.FC = () => { + + ID + User @@ -121,13 +106,14 @@ const FamilyTableComponent: React.FC = () => { Usage - ID + Quota {familyMembers.map((member) => ( + {member.id} {member.email} { {formatUsageToGB(member.usage)} - {member.id} + + {member.status !== "SELF" + ? (member.storageLimit && + formatUsageToGB( + member.storageLimit, + )) || + "NA" + : ""} + ))} diff --git a/infra/staff/src/components/ToggleEmailMFA.tsx b/infra/staff/src/components/ToggleEmailMFA.tsx index 0e0a469a73..82e3f28015 100644 --- a/infra/staff/src/components/ToggleEmailMFA.tsx +++ b/infra/staff/src/components/ToggleEmailMFA.tsx @@ -10,14 +10,7 @@ import { import React, { useState } from "react"; import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions import { apiOrigin } from "../services/support"; - -interface UserData { - subscription?: { - userID: string; - // Add other properties as per your API response structure - }; - // Add other properties as per your API response structure -} +import type { UserData } from "../types"; interface ToggleEmailMFAProps { open: boolean; diff --git a/infra/staff/src/components/UpdateSubscription.tsx b/infra/staff/src/components/UpdateSubscription.tsx index b1bf7dfea1..1b1806b59a 100644 --- a/infra/staff/src/components/UpdateSubscription.tsx +++ b/infra/staff/src/components/UpdateSubscription.tsx @@ -62,8 +62,8 @@ const UpdateSubscription: React.FC = ({ expiryTime: "", userId: "", attributes: { - "customerID": "", - "stripeAccountCountry": "" + customerID: "", + stripeAccountCountry: "", }, }); @@ -108,9 +108,13 @@ const UpdateSubscription: React.FC = ({ expiryTime: expiryTime, userId: userDataResponse.subscription.userID || "", attributes: { - customerID: userDataResponse.subscription.attributes.customerID || "", - stripeAccountCountry: userDataResponse.subscription.attributes.stripeAccountCountry || "" - } + customerID: + userDataResponse.subscription.attributes + .customerID || "", + stripeAccountCountry: + userDataResponse.subscription.attributes + .stripeAccountCountry || "", + }, }); } catch (error) { console.error("Error fetching data:", error); @@ -174,8 +178,9 @@ const UpdateSubscription: React.FC = ({ transactionId: values.transactionId, attributes: { customerID: values.attributes.customerID, - stripeAccountCountry: values.attributes.stripeAccountCountry - } + stripeAccountCountry: + values.attributes.stripeAccountCountry, + }, }; try { diff --git a/infra/staff/src/components/UserComponent.tsx b/infra/staff/src/components/UserComponent.tsx index bc93101960..be2b1afb11 100644 --- a/infra/staff/src/components/UserComponent.tsx +++ b/infra/staff/src/components/UserComponent.tsx @@ -13,6 +13,7 @@ import TableContainer from "@mui/material/TableContainer"; import TableRow from "@mui/material/TableRow"; import Typography from "@mui/material/Typography"; import * as React from "react"; +import type { UserComponentProps } from "../types"; import ChangeEmail from "./ChangeEmail"; import DeleteAccount from "./DeleteAccont"; import Disable2FA from "./Disable2FA"; @@ -20,17 +21,6 @@ import DisablePasskeys from "./DisablePasskeys"; import ToggleEmailMFA from "./ToggleEmailMFA"; import UpdateSubscription from "./UpdateSubscription"; -export interface UserData { - User: Record; - Storage: Record; - Subscription: Record; - Security: Record; -} - -interface UserComponentProps { - userData: UserData | null; -} - const UserComponent: React.FC = ({ userData }) => { const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false); const [email2FAEnabled, setEmail2FAEnabled] = React.useState(false); @@ -44,10 +34,10 @@ const UserComponent: React.FC = ({ userData }) => { const [disablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false); React.useEffect(() => { - setTwoFactorEnabled(userData?.Security["Two factor 2FA"] === "Enabled"); - setEmail2FAEnabled(userData?.Security["Email MFA"] === "Enabled"); + setTwoFactorEnabled(userData?.security["Two factor 2FA"] === "Enabled"); + setEmail2FAEnabled(userData?.security["Email MFA"] === "Enabled"); setCanDisableEmailMFA( - userData?.Security["Can Disable EmailMFA"] === "Yes", + userData?.security["Can Disable EmailMFA"] === "Yes", ); }, [userData]); @@ -148,14 +138,10 @@ const DataTable: React.FC = ({ minHeight: 300, display: "flex", flexDirection: "column", - marginBottom: "20px", height: "100%", width: "100%", - padding: "13px", + padding: "10px", overflowX: "hidden", - "&:not(:last-child)": { - marginBottom: "40px", - }, }} > = ({ width: "100%", }} > - {title} + {title.charAt(0).toUpperCase() + title.slice(1)} - {title === "User" && ( + {title === "user" && ( = ({ )} - {title === "Subscription" && ( + {title === "subscription" && ( ; + storage: Record; + subscription?: Record; + security: Record; + details?: { + familyData: { + members: FamilyMember[]; + }; + }; +} + +export interface UserComponentProps { + userData: UserData | null; +} + +// Error Response Interface +export interface ErrorResponse { + message: string; +} + +// Types related to Subscriptions +export interface Subscription { + productID: string; + paymentProvider: string; + expiryTime: number; + storage: number; +} + +export interface Security { + isEmailMFAEnabled: boolean; + isTwoFactorEnabled: boolean; + passkeys: string; + passkeyCount: number; + canDisableEmailMFA: boolean; +} + +// Types related Family +export interface FamilyMember { + id: string; + email: string; + status: string; + usage: number; + storageLimit: number; +} + +// Types related to passkeys +export interface DisablePasskeysProps { + open: boolean; + handleClose: () => void; + handleDisablePasskeys: () => void; // Callback to handle disabling passkeys +} diff --git a/infra/staff/src/utils/index.ts b/infra/staff/src/utils/index.ts new file mode 100644 index 0000000000..46a4bc3fcf --- /dev/null +++ b/infra/staff/src/utils/index.ts @@ -0,0 +1,5 @@ +// Common utilities +export function formatUsageToGB(usage: number): string { + const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2); + return `${usageInGB} GB`; +} diff --git a/infra/staff/src/utils/strings.ts b/infra/staff/src/utils/strings.ts deleted file mode 100644 index 39b22fcd76..0000000000 --- a/infra/staff/src/utils/strings.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * User facing strings in the app. - * - * By keeping them separate, we make our lives easier if/when we need to - * localize the corresponding pages. Right now, these are just the values in the - * default language, English. - */ -const S = { - hello: "Hello Ente!", - error_generic: "Oops, something went wrong.", -}; - -export default S; diff --git a/infra/workers/cast-albums/src/index.ts b/infra/workers/cast-albums/src/index.ts index 5111d446cd..fc93d93443 100644 --- a/infra/workers/cast-albums/src/index.ts +++ b/infra/workers/cast-albums/src/index.ts @@ -22,7 +22,8 @@ const handleOPTIONS = (request: Request) => { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Max-Age": "86400", - "Access-Control-Allow-Headers": "X-Cast-Access-Token", + "Access-Control-Allow-Headers": + "X-Cast-Access-Token, X-Client-Package, X-Client-Version", }, }); }; @@ -60,8 +61,15 @@ const handleGET = async (request: Request) => { const pathname = url.pathname; const params = new URLSearchParams({ castToken }); + const headers = { + "X-Client-Package": request.headers.get("X-Client-Package") ?? "", + "X-Client-Version": request.headers.get("X-Client-Version") ?? "", + "User-Agent": request.headers.get("User-Agent") ?? "", + }; + let response = await fetch( `https://api.ente.io/cast/files${pathname}${fileID}?${params.toString()}`, + { headers }, ); if (!response.ok) console.log("Upstream error", response.status); diff --git a/infra/workers/cast-albums/wrangler.toml b/infra/workers/cast-albums/wrangler.toml index f81f8b52bf..3496aebe7a 100644 --- a/infra/workers/cast-albums/wrangler.toml +++ b/infra/workers/cast-albums/wrangler.toml @@ -1,6 +1,6 @@ name = "cast-albums" main = "src/index.ts" -compatibility_date = "2024-06-14" +compatibility_date = "2025-06-03" routes = [ { pattern = "cast-albums.ente.io", custom_domain = true } diff --git a/infra/workers/csp-reporter/package.json b/infra/workers/csp-reporter/package.json new file mode 100644 index 0000000000..80c9a9d516 --- /dev/null +++ b/infra/workers/csp-reporter/package.json @@ -0,0 +1,5 @@ +{ + "name": "csp-reporter", + "version": "0.0.0", + "private": true +} diff --git a/infra/workers/csp-reporter/src/index.ts b/infra/workers/csp-reporter/src/index.ts new file mode 100644 index 0000000000..34af4cd68e --- /dev/null +++ b/infra/workers/csp-reporter/src/index.ts @@ -0,0 +1,23 @@ +/** + * Log CSP reports. + * + * See _headers in the web app source. + */ + +export default { + async fetch(request: Request) { + switch (request.method) { + case "POST": + return handlePOST(request); + default: + console.log(`Unsupported HTTP method ${request.method}`); + return new Response(null, { status: 405 }); + } + }, +} satisfies ExportedHandler; + +const handlePOST = async (request: Request) => { + // {job="worker"} |= `[csp-report]` | json log="logs[0]" | keep log + console.log("[csp-report]", await request.text()); + return new Response(null, { status: 200 }); +}; diff --git a/infra/workers/csp-reporter/tsconfig.json b/infra/workers/csp-reporter/tsconfig.json new file mode 100644 index 0000000000..a65b752070 --- /dev/null +++ b/infra/workers/csp-reporter/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../tsconfig.base.json", "include": ["src"] } diff --git a/infra/workers/csp-reporter/wrangler.toml b/infra/workers/csp-reporter/wrangler.toml new file mode 100644 index 0000000000..8cdc2f9682 --- /dev/null +++ b/infra/workers/csp-reporter/wrangler.toml @@ -0,0 +1,9 @@ +name = "csp-reporter" +main = "src/index.ts" +compatibility_date = "2025-05-19" + +routes = [ + { pattern = "csp-reporter.ente.io", custom_domain = true } +] + +tail_consumers = [{ service = "tail" }] diff --git a/infra/workers/files/src/index.ts b/infra/workers/files/src/index.ts index e855dca243..904a669acc 100644 --- a/infra/workers/files/src/index.ts +++ b/infra/workers/files/src/index.ts @@ -21,7 +21,8 @@ const handleOPTIONS = (request: Request) => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package", + "Access-Control-Allow-Headers": + "X-Auth-Token, X-Client-Package, X-Client-Version, Range", "Access-Control-Max-Age": "86400", }, }); @@ -71,13 +72,16 @@ const handleGET = async (request: Request) => { const params = new URLSearchParams(); if (token) params.set("token", token); + const headers = { + "X-Client-Package": request.headers.get("X-Client-Package") ?? "", + "X-Client-Version": request.headers.get("X-Client-Version") ?? "", + "User-Agent": request.headers.get("User-Agent") ?? "", + "Range": request.headers.get("Range") ?? "", + }; + let response = await fetch( `https://api.ente.io/files/download/${fileID}?${params.toString()}`, - { - headers: { - "User-Agent": request.headers.get("User-Agent") ?? "", - }, - }, + { headers }, ); if (!response.ok) console.log("Upstream error", response.status); diff --git a/infra/workers/files/wrangler.toml b/infra/workers/files/wrangler.toml index 52349d8d03..12e27ade88 100644 --- a/infra/workers/files/wrangler.toml +++ b/infra/workers/files/wrangler.toml @@ -1,6 +1,6 @@ name = "files" main = "src/index.ts" -compatibility_date = "2024-06-14" +compatibility_date = "2025-06-03" routes = [ { pattern = "files.ente.io", custom_domain = true } diff --git a/infra/workers/package.json b/infra/workers/package.json index d9a555362f..7b61f25bd8 100644 --- a/infra/workers/package.json +++ b/infra/workers/package.json @@ -2,10 +2,10 @@ "name": "workers", "private": true, "devDependencies": { - "@cloudflare/workers-types": "^4.20240614.0", - "typescript": "^5", - "wrangler": "^3", - "prettier": "^3.3.4" + "@cloudflare/workers-types": "^4.20250603.0", + "typescript": "^5.8.3", + "wrangler": "^4.18.0", + "prettier": "^3.5.3" }, "workspaces": [ "*" diff --git a/infra/workers/public-albums/src/index.ts b/infra/workers/public-albums/src/index.ts index 505a474635..75751a867a 100644 --- a/infra/workers/public-albums/src/index.ts +++ b/infra/workers/public-albums/src/index.ts @@ -22,7 +22,7 @@ const handleOPTIONS = (request: Request) => { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": - "X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package", + "X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package, X-Client-Version", "Access-Control-Max-Age": "86400", }, }); @@ -70,8 +70,15 @@ const handleGET = async (request: Request) => { if (accessToken) params.set("accessToken", accessToken); if (accessTokenJWT) params.set("accessTokenJWT", accessTokenJWT); + const headers = { + "X-Client-Package": request.headers.get("X-Client-Package") ?? "", + "X-Client-Version": request.headers.get("X-Client-Version") ?? "", + "User-Agent": request.headers.get("User-Agent") ?? "", + }; + let response = await fetch( `https://api.ente.io/public-collection/files${pathname}${fileID}?${params.toString()}`, + { headers }, ); if (!response.ok) console.log("Upstream error", response.status); diff --git a/infra/workers/public-albums/wrangler.toml b/infra/workers/public-albums/wrangler.toml index 9adad20f04..4643736fd6 100644 --- a/infra/workers/public-albums/wrangler.toml +++ b/infra/workers/public-albums/wrangler.toml @@ -1,6 +1,6 @@ name = "public-albums" main = "src/index.ts" -compatibility_date = "2024-06-14" +compatibility_date = "2025-06-03" routes = [ { pattern = "public-albums.ente.io", custom_domain = true } diff --git a/infra/workers/thumbnails/src/index.ts b/infra/workers/thumbnails/src/index.ts index 9fc23fa52b..2e2bd89733 100644 --- a/infra/workers/thumbnails/src/index.ts +++ b/infra/workers/thumbnails/src/index.ts @@ -21,7 +21,8 @@ const handleOPTIONS = (request: Request) => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package", + "Access-Control-Allow-Headers": + "X-Auth-Token, X-Client-Package, X-Client-Version", "Access-Control-Max-Age": "86400", }, }); @@ -64,8 +65,15 @@ const handleGET = async (request: Request) => { const params = new URLSearchParams(); if (token) params.set("token", token); + const headers = { + "X-Client-Package": request.headers.get("X-Client-Package") ?? "", + "X-Client-Version": request.headers.get("X-Client-Version") ?? "", + "User-Agent": request.headers.get("User-Agent") ?? "", + }; + let response = await fetch( `https://api.ente.io/files/preview/${fileID}?${params.toString()}`, + { headers }, ); if (!response.ok) console.log("Upstream error", response.status); diff --git a/infra/workers/thumbnails/wrangler.toml b/infra/workers/thumbnails/wrangler.toml index 8f45b859e8..8c7117c848 100644 --- a/infra/workers/thumbnails/wrangler.toml +++ b/infra/workers/thumbnails/wrangler.toml @@ -1,6 +1,6 @@ name = "thumbnails" main = "src/index.ts" -compatibility_date = "2024-06-14" +compatibility_date = "2025-06-03" routes = [ { pattern = "thumbnails.ente.io", custom_domain = true } diff --git a/infra/workers/uploader/src/index.ts b/infra/workers/uploader/src/index.ts index fb811924be..62396e5c8d 100644 --- a/infra/workers/uploader/src/index.ts +++ b/infra/workers/uploader/src/index.ts @@ -28,7 +28,7 @@ const handleOPTIONS = (request: Request) => { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, PUT, OPTIONS", "Access-Control-Allow-Headers": - "Content-Type, UPLOAD-URL, X-Client-Package", + "Content-Type, UPLOAD-URL, X-Client-Package, X-Client-Version", "Access-Control-Expose-Headers": "X-Request-Id, CF-Ray", "Access-Control-Max-Age": "86400", }, diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index a5fe360712..2d1fb835f0 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -53,7 +53,7 @@ analyzer: sort_child_properties_last: warning sort_pub_dependencies: warning library_private_types_in_public_api: warning - constant_identifier_names: warning + constant_identifier_names: ignore prefer_const_constructors: warning prefer_const_declarations: warning prefer_const_constructors_in_immutables: warning diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 60f760e2c4..4583051e80 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,10 @@ android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" - android:largeHeap="true"> + android:dataExtractionRules="@xml/data_extraction_rules" + android:largeHeap="true" + android:networkSecurityConfig="@xml/network_security_config" + > + + + + + + + + + + + + + + + + + + + + + @@ -177,4 +203,6 @@ + + \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt new file mode 100644 index 0000000000..147d5ddea6 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteAlbumsWidgetProvider.kt @@ -0,0 +1,195 @@ +package io.ente.photos + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider +import java.io.File +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Serializable +data class AlbumsFileData( + val title: String?, + val subText: String?, + val generatedId: Int?, + val mainKey: String? +) + +class EnteAlbumsWidgetProvider : HomeWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + appWidgetIds.forEach { widgetId -> + val views = + RemoteViews(context.packageName, R.layout.albums_widget_layout) + .apply { + val totalAlbums = + widgetData.getInt("totalAlbums", 0) + var randomNumber = -1 + var imagePath: String? = null + if (totalAlbums > 0) { + randomNumber = + (0 until totalAlbums!!).random() + imagePath = + widgetData.getString( + "albums_widget_" + + randomNumber, + null + ) + } + var imageExists: Boolean = false + if (imagePath != null) { + val imageFile = File(imagePath) + imageExists = imageFile.exists() + } + if (imageExists) { + val data = + widgetData.getString( + "albums_widget_${randomNumber}_data", + null + ) + val decoded: AlbumsFileData? = + data?.let { + Json.decodeFromString< + AlbumsFileData>(it) + } + val title = decoded?.title + val subText = decoded?.subText + val generatedId = decoded?.generatedId + val mainKey = decoded?.mainKey + + val deepLinkUri = + Uri.parse( + "albumwidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget" + ) + + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + deepLinkUri + ) + + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EnteAlbumsWidgetProvider", + "Image exists: $imagePath" + ) + setViewVisibility( + R.id.widget_img, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_subtitle, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_title, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_overlay, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.GONE + ) + + val bitmap: Bitmap = + BitmapFactory.decodeFile(imagePath) + setImageViewBitmap(R.id.widget_img, bitmap) + setTextViewText(R.id.widget_title, title) + setTextViewText( + R.id.widget_subtitle, + subText + ) + } else { + // Open App on Widget Click + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EnteAlbumsWidgetProvider", + "Image doesn't exists" + ) + setViewVisibility( + R.id.widget_img, + View.GONE + ) + setViewVisibility( + R.id.widget_subtitle, + View.GONE + ) + setViewVisibility( + R.id.widget_title, + View.GONE + ) + setViewVisibility( + R.id.widget_overlay, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.VISIBLE + ) + + val drawable = + ContextCompat.getDrawable( + context, + R.drawable.ic_albums_widget + ) + val bitmap = + (drawable as BitmapDrawable).bitmap + setImageViewBitmap( + R.id.widget_placeholder, + bitmap + ) + } + } + + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt index a2a4e09bf7..8e9c0f9d4a 100644 --- a/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EnteMemoryWidgetProvider.kt @@ -91,10 +91,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { R.id.widget_img, View.VISIBLE ) - setViewVisibility( - R.id.widget_placeholder_container, - View.VISIBLE - ) setViewVisibility( R.id.widget_subtitle, View.VISIBLE @@ -148,10 +144,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { R.id.widget_img, View.GONE ) - setViewVisibility( - R.id.widget_placeholder_container, - View.GONE - ) setViewVisibility( R.id.widget_subtitle, View.GONE @@ -181,7 +173,7 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() { ContextCompat.getDrawable( context, R.drawable - .ic_home_widget_default + .ic_memories_widget ) val bitmap = (drawable as BitmapDrawable).bitmap diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt new file mode 100644 index 0000000000..70f15a518c --- /dev/null +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/EntePeopleWidgetProvider.kt @@ -0,0 +1,195 @@ +package io.ente.photos + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider +import java.io.File +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Serializable +data class PeopleFileData( + val title: String?, + val subText: String?, + val generatedId: Int?, + val mainKey: String? +) + +class EntePeopleWidgetProvider : HomeWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + appWidgetIds.forEach { widgetId -> + val views = + RemoteViews(context.packageName, R.layout.people_widget_layout) + .apply { + val totalPeople = + widgetData.getInt("totalPeople", 0) + var randomNumber = -1 + var imagePath: String? = null + if (totalPeople > 0) { + randomNumber = + (0 until totalPeople!!).random() + imagePath = + widgetData.getString( + "people_widget_" + + randomNumber, + null + ) + } + var imageExists: Boolean = false + if (imagePath != null) { + val imageFile = File(imagePath) + imageExists = imageFile.exists() + } + if (imageExists) { + val data = + widgetData.getString( + "people_widget_${randomNumber}_data", + null + ) + val decoded: PeopleFileData? = + data?.let { + Json.decodeFromString< + PeopleFileData>(it) + } + val title = decoded?.title + val subText = decoded?.subText + val generatedId = decoded?.generatedId + val mainKey = decoded?.mainKey + + val deepLinkUri = + Uri.parse( + "peoplewidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget" + ) + + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + deepLinkUri + ) + + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EntePeopleWidgetProvider", + "Image exists: $imagePath" + ) + setViewVisibility( + R.id.widget_img, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_subtitle, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_title, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_overlay, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.GONE + ) + + val bitmap: Bitmap = + BitmapFactory.decodeFile(imagePath) + setImageViewBitmap(R.id.widget_img, bitmap) + setTextViewText(R.id.widget_title, title) + setTextViewText( + R.id.widget_subtitle, + subText + ) + } else { + // Open App on Widget Click + val pendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent( + R.id.widget_container, + pendingIntent + ) + + Log.d( + "EntePeopleWidgetProvider", + "Image doesn't exists" + ) + setViewVisibility( + R.id.widget_img, + View.GONE + ) + setViewVisibility( + R.id.widget_subtitle, + View.GONE + ) + setViewVisibility( + R.id.widget_title, + View.GONE + ) + setViewVisibility( + R.id.widget_overlay, + View.GONE + ) + setViewVisibility( + R.id.widget_placeholder, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_text, + View.VISIBLE + ) + setViewVisibility( + R.id.widget_placeholder_container, + View.VISIBLE + ) + + val drawable = + ContextCompat.getDrawable( + context, + R.drawable.ic_people_widget + ) + val bitmap = + (drawable as BitmapDrawable).bitmap + setImageViewBitmap( + R.id.widget_placeholder, + bitmap + ) + } + } + + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png new file mode 100644 index 0000000000..41ba0108a9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png deleted file mode 100644 index 449ea27a83..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png new file mode 100644 index 0000000000..a5f302a320 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png new file mode 100644 index 0000000000..644b416026 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png new file mode 100644 index 0000000000..a9b5792d31 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png deleted file mode 100644 index 4ae54e4339..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png new file mode 100644 index 0000000000..fdaa8e7d53 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png new file mode 100644 index 0000000000..1044dc6b02 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..b590f07b24 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png deleted file mode 100644 index f158f6a086..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..25db5c73b6 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png new file mode 100644 index 0000000000..04e74c6d0b Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..e501c7552b Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png deleted file mode 100644 index 224d849f3b..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..f42319fb19 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png new file mode 100644 index 0000000000..4a9122ab54 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png new file mode 100644 index 0000000000..7ffe666ae9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_albums_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png deleted file mode 100644 index 066e6e362a..0000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png new file mode 100644 index 0000000000..616d6f3148 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_memories_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png new file mode 100644 index 0000000000..f935479c04 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_people_widget.png differ diff --git a/mobile/android/app/src/main/res/drawable/albums_widget_preview.png b/mobile/android/app/src/main/res/drawable/albums_widget_preview.png new file mode 100644 index 0000000000..5a7d069ed0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/albums_widget_preview.png differ diff --git a/mobile/android/app/src/main/res/drawable/people_widget_preview.png b/mobile/android/app/src/main/res/drawable/people_widget_preview.png new file mode 100644 index 0000000000..7501d68098 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/people_widget_preview.png differ diff --git a/mobile/android/app/src/main/res/layout/albums_widget_layout.xml b/mobile/android/app/src/main/res/layout/albums_widget_layout.xml new file mode 100644 index 0000000000..aa63ca753f --- /dev/null +++ b/mobile/android/app/src/main/res/layout/albums_widget_layout.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/layout/memory_widget_layout.xml b/mobile/android/app/src/main/res/layout/memory_widget_layout.xml index abda81b191..71a7b44089 100644 --- a/mobile/android/app/src/main/res/layout/memory_widget_layout.xml +++ b/mobile/android/app/src/main/res/layout/memory_widget_layout.xml @@ -65,15 +65,15 @@ android:id="@+id/widget_placeholder" android:layout_width="120dp" android:layout_height="120dp" - android:src="@drawable/ic_home_widget_default" + android:src="@drawable/ic_memories_widget" android:scaleType="fitCenter" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/xml/albums_widget.xml b/mobile/android/app/src/main/res/xml/albums_widget.xml new file mode 100644 index 0000000000..464b74db66 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/albums_widget.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/data_extraction_rules.xml b/mobile/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..16da40e6d4 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/network_security_config.xml b/mobile/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..a7a11ec732 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + ente.io + + + + + diff --git a/mobile/android/app/src/main/res/xml/people_widget.xml b/mobile/android/app/src/main/res/xml/people_widget.xml new file mode 100644 index 0000000000..a6c37ba398 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/people_widget.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index d4d4693541..5f335a8ac0 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -12,6 +12,9 @@ allprojects { maven { url "${project(':background_fetch').projectDir}/libs" } + maven { + url "${project(':ffmpeg_kit_flutter').projectDir}/libs" + } } } diff --git a/mobile/assets/2.0x/albums-widget-static.png b/mobile/assets/2.0x/albums-widget-static.png new file mode 100644 index 0000000000..00bb2ba52a Binary files /dev/null and b/mobile/assets/2.0x/albums-widget-static.png differ diff --git a/mobile/assets/2.0x/memories-widget-static.png b/mobile/assets/2.0x/memories-widget-static.png new file mode 100644 index 0000000000..1e079f1e99 Binary files /dev/null and b/mobile/assets/2.0x/memories-widget-static.png differ diff --git a/mobile/assets/2.0x/people-widget-static.png b/mobile/assets/2.0x/people-widget-static.png new file mode 100644 index 0000000000..1e619b26e9 Binary files /dev/null and b/mobile/assets/2.0x/people-widget-static.png differ diff --git a/mobile/assets/3.0x/albums-widget-static.png b/mobile/assets/3.0x/albums-widget-static.png new file mode 100644 index 0000000000..9826a8d8c2 Binary files /dev/null and b/mobile/assets/3.0x/albums-widget-static.png differ diff --git a/mobile/assets/3.0x/memories-widget-static.png b/mobile/assets/3.0x/memories-widget-static.png new file mode 100644 index 0000000000..dc0cdef537 Binary files /dev/null and b/mobile/assets/3.0x/memories-widget-static.png differ diff --git a/mobile/assets/3.0x/people-widget-static.png b/mobile/assets/3.0x/people-widget-static.png new file mode 100644 index 0000000000..aac3ee642c Binary files /dev/null and b/mobile/assets/3.0x/people-widget-static.png differ diff --git a/mobile/assets/albums-widget-static.png b/mobile/assets/albums-widget-static.png new file mode 100644 index 0000000000..9e6ddcf832 Binary files /dev/null and b/mobile/assets/albums-widget-static.png differ diff --git a/mobile/assets/icons/albums-widget-icon.svg b/mobile/assets/icons/albums-widget-icon.svg new file mode 100644 index 0000000000..d42448bcef --- /dev/null +++ b/mobile/assets/icons/albums-widget-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/memories-widget-icon.svg b/mobile/assets/icons/memories-widget-icon.svg new file mode 100644 index 0000000000..7a72222cc7 --- /dev/null +++ b/mobile/assets/icons/memories-widget-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile/assets/icons/past-year-memory-icon.svg b/mobile/assets/icons/past-year-memory-icon.svg new file mode 100644 index 0000000000..4c50152714 --- /dev/null +++ b/mobile/assets/icons/past-year-memory-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/people-widget-icon.svg b/mobile/assets/icons/people-widget-icon.svg new file mode 100644 index 0000000000..3cd1ee3972 --- /dev/null +++ b/mobile/assets/icons/people-widget-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile/assets/icons/smart-memory-icon.svg b/mobile/assets/icons/smart-memory-icon.svg new file mode 100644 index 0000000000..cdc2d89b59 --- /dev/null +++ b/mobile/assets/icons/smart-memory-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mobile/assets/memories-widget-static.png b/mobile/assets/memories-widget-static.png new file mode 100644 index 0000000000..4f7f546ad7 Binary files /dev/null and b/mobile/assets/memories-widget-static.png differ diff --git a/mobile/assets/people-widget-static.png b/mobile/assets/people-widget-static.png new file mode 100644 index 0000000000..038f8eeb81 Binary files /dev/null and b/mobile/assets/people-widget-static.png differ diff --git a/mobile/fastlane/metadata/android/id/full_description.txt b/mobile/fastlane/metadata/android/id/full_description.txt index dc5c077a52..7b3deb7985 100644 --- a/mobile/fastlane/metadata/android/id/full_description.txt +++ b/mobile/fastlane/metadata/android/id/full_description.txt @@ -1,4 +1,4 @@ -ente merupakan app sederhana yang memungkinkanmu untuk mencadangkan serta membagikan foto dan video. +ente adalah aplikasi sederhana untuk mencadangkan serta membagikan foto dan video anda. Jika kamu ingin alternatif Google Photos yang ramah privasi, kamu menemukan app yang tepat. Dengan ente, kenanganmu terenkripsi dari ujung ke ujung (e2ee). Sehingga, hanya kamu yang dapat melihatnya. diff --git a/mobile/fastlane/metadata/android/pt_PT/full_description.txt b/mobile/fastlane/metadata/android/pt_PT/full_description.txt index 38a7c1b83f..9cd11069c4 100644 --- a/mobile/fastlane/metadata/android/pt_PT/full_description.txt +++ b/mobile/fastlane/metadata/android/pt_PT/full_description.txt @@ -4,33 +4,33 @@ Se busca por uma alternativa do Google Fotos baseada em privacidade, chegaste ao Temos aplicações de fonte aberta para Android, iOS, sítio web e desktop, e as fotos serão perfeitamente sincronizadas entre todas elas numa maneira de encriptação de ponta a ponta (e2ee). -Ente também simplifica a partilha dos seus álbuns com os seus entes queridos, mesmo que estes não estejam no ente. Pode partilhar ligações visíveis publicamente, onde podem ver o seu álbum e colaborar adicionando fotografias ao mesmo, mesmo sem uma conta ou aplicação. +Ente facilita o partilhamento dos seus álbuns com entes queridos, mesmo se não estiverem no ente. Pode partilhar ligações visíveis a público, onde eles podem ver o seu álbum e colaborar a adicionar fotos, mesmo sem uma conta ou a aplicação. -Os seus dados encriptados são replicados em 3 locais diferentes, incluindo um abrigo de emergência em Paris. Levamos a posteridade a sério e facilitamos a tarefa de garantir que as suas memórias perdurem para além de si. +Os dados são replicados em 3 localizações diferentes, incluindo um posto em Paris. Levamos a nossa postura a sério e facilitamos para certificarmos que as suas memórias revivam-no. -Estamos aqui para criar a aplicação de fotografias mais segura de sempre, junte-se à nossa viagem! +Estamos aqui para fazer a aplicação mais segura do mundo, venha e adere a nossa jornada! -RECURSOS -- Cópias de segurança de qualidade original, porque cada pixel é importante -- Planos familiares, para que possa partilhar o armazenamento com a sua família -- Álbuns colaborativos, para que possa reunir fotos depois de uma viagem -- Pastas partilhadas, caso queira que o seu parceiro desfrute dos seus cliques na “Câmara” -- Links para álbuns, que podem ser protegidas com uma palavra-passe -- Capacidade de libertar espaço, removendo ficheiros dos quais foi feita uma cópia de segurança segura -- Apoio humano, porque vale a pena -- Descrições, para que possa legendar as suas memórias e encontrá-las facilmente -- Editor de imagens, para dar os retoques finais -- Favoritar, ocultar e reviver suas memórias, pois elas são preciosas -- Importação com um clique do Google, da Apple, do seu disco rígido e muito mais -- Tema escuro, porque as suas fotos ficam bem com ele +FUNCIONALIDADES +- Backups com qualidade original, por cada píxel valer a pena +- Planos em família, para poder partilhar armazenamento com familiares +- Álbuns de colaboração, para unir fotos depois de uma caminhada +- Pastas partilhadas, se quiser que o seu parceiro desfrute dos seus cliques na "Câmara" +- Ligações para álbuns, que podem ser protegidos com uma palavra-passe +- Possibilidade de liberar espaço, removendo ficheiros que já foram feitos backup +- Suporte físico, por valer a pena +- Descrições, para entender as suas memórias e encontrá-las facilmente +- Editor de imagens, para dar retoques finais +- Adicionar aos favoritos, obscurecer e reviver as suas memórias, para aqueles tão preciosos +- Importar num só clique do Google, Apple, e o seu disco rígido e mais +- Tema escuro, para as suas fotos encaixarem melhor - 2FA, 3FA, autenticação biométrica -- e MUITO mais! +- e MAIS além! PERMISSÕES -ente solicita determinadas permissões para servir o objetivo de um fornecedor de armazenamento de fotografias, que pode ser consultado aqui: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md +Ente pede por certas permissões para servir o propósito dum provedor de armazenamento de foto, onde pode ser revisto aqui: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PREÇO -Não oferecemos planos gratuitos para sempre, porque é importante para nós mantermo-nos sustentáveis e resistirmos ao teste do tempo. Em vez disso, oferecemos planos acessíveis que pode partilhar livremente com a sua família. Pode encontrar mais informações em ente.io. +Não garantimos planos gratuitos para sempre, já que é importante a nós mantermo-nos sustentáveis e conseguirmos superar o desafio do tempo. Ao invés, garantimos planos acessíveis para poder partilhar livremente com os seus familiares. Para mais informações, consulte "ente.io" -SUPPORT -Orgulhamo-nos de oferecer um apoio humano Se for nosso cliente pago, pode contactar team@ente.io e esperar uma resposta da nossa equipa no prazo de 24 horas. +SUPORTE +Estamos orgulhosos ao oferecer suporte físico. Se for um cliente pago, pode contactar a nossa equipa através de "team@ente.io" e esperar uma resposta nossa dentro de um dia. diff --git a/mobile/fastlane/metadata/android/sr/full_description.txt b/mobile/fastlane/metadata/android/sr/full_description.txt new file mode 100644 index 0000000000..9ba4fe3143 --- /dev/null +++ b/mobile/fastlane/metadata/android/sr/full_description.txt @@ -0,0 +1,36 @@ +ente is a simple app to backup and share your photos and videos. + +If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. + +We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner. + +ente also makes it simple to share your albums with your loved ones, even if they aren't on ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app. + +Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you. + +We are here to make the safest photos app ever, come join our journey! + +FEATURES +- Original quality backups, because every pixel is important +- Family plans, so you can share storage with your family +- Collaborative albums, so you can pool together photos after a trip +- Shared folders, in case you want your partner to enjoy your "Camera" clicks +- Album links, that can be protected with a password +- Ability to free up space, by removing files that have been safely backed up +- Human support, because you're worth it +- Descriptions, so you can caption your memories and find them easily +- Image editor, to add finishing touches +- Favorite, hide and relive your memories, for they are precious +- One-click import from Google, Apple, your hard drive and more +- Dark theme, because your photos look good in it +- 2FA, 3FA, biometric auth +- and a LOT more! + +PERMISSIONS +ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md + +PRICING +We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io. + +SUPPORT +We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours. diff --git a/mobile/fastlane/metadata/android/sr/short_description.txt b/mobile/fastlane/metadata/android/sr/short_description.txt new file mode 100644 index 0000000000..7a5fe973db --- /dev/null +++ b/mobile/fastlane/metadata/android/sr/short_description.txt @@ -0,0 +1 @@ +ente is an end-to-end encrypted photo storage app \ No newline at end of file diff --git a/mobile/fastlane/metadata/android/sr/title.txt b/mobile/fastlane/metadata/android/sr/title.txt new file mode 100644 index 0000000000..3a4fed48fe --- /dev/null +++ b/mobile/fastlane/metadata/android/sr/title.txt @@ -0,0 +1 @@ +ente - encrypted photo storage \ No newline at end of file diff --git a/mobile/fastlane/metadata/android/tr/full_description.txt b/mobile/fastlane/metadata/android/tr/full_description.txt index a978db8478..b3da5caaee 100644 --- a/mobile/fastlane/metadata/android/tr/full_description.txt +++ b/mobile/fastlane/metadata/android/tr/full_description.txt @@ -15,7 +15,7 @@ ente ayrıca, ente'de olmasalar bile albümlerinizi sevdiklerinizle paylaşmanı - Aile planları, böylece depolama alanını ailenizle paylaşabilirsiniz - Seyahatten sonra fotoğrafları bir araya toplayabilmeniz için ortak albümler - Partnerinizin "Kamera" tıklamalarınızın keyfini çıkarmasını istemeniz durumunda paylaşılan klasörler -- Parola ile korunabilen ve süresi dolacak şekilde ayarlanabilen albüm bağlantıları +- Albüm bağlantıları, parolayla korunabilir - Güvenli bir şekilde yedeklenmiş dosyaları kaldırarak alan boşaltma yeteneği - İnsan desteği, çünkü sen buna değersin - Açıklamalar, böylece anılarınıza başlık yazabilir ve onları kolayca bulabilirsiniz @@ -27,10 +27,10 @@ ente ayrıca, ente'de olmasalar bile albümlerinizi sevdiklerinizle paylaşmanı - ve çok daha fazlası! İZİNLER -bir fotoğraf depolama sağlayıcısının amacına hizmet etmek için belirli izinlere yönelik taleplerde bulunulabilir; bu izinler burada incelenebilir: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md +ente, burada gözden geçirilebilecek bir fotoğraf depolama sağlayıcısının amacına hizmet etmek için belirli izinler ister: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md FİYATLANDIRMA Sonsuza kadar ücretsiz planlar sunmuyoruz, çünkü sürdürülebilir kalmamız ve zamanın testine dayanmamız bizim için önemli. Bunun yerine, ailenizle özgürce paylaşabileceğiniz uygun fiyatlı planlar sunuyoruz. Daha fazla bilgiyi ente.io adresinde bulabilirsiniz. -🙋DESTEK +DESTEK İnsan desteği sunmaktan gurur duyuyoruz. Ücretli müşterimiz iseniz team@ente.io adresine ulaşabilir ve ekibimizden 24 saat içinde yanıt bekleyebilirsiniz. diff --git a/mobile/fastlane/metadata/ios/sr/description.txt b/mobile/fastlane/metadata/ios/sr/description.txt new file mode 100644 index 0000000000..a98a74300a --- /dev/null +++ b/mobile/fastlane/metadata/ios/sr/description.txt @@ -0,0 +1,33 @@ +Ente is a simple app to automatically backup and organize your photos and videos. + +If you've been looking for a privacy-friendly alternative to preserve your memories, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. + +We have apps across all platforms, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner. + +Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links. + +Your encrypted data is stored across multiple locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you. + +We are here to make the safest photos app ever, come join our journey! + +FEATURES +- Original quality backups, because every pixel is important +- Family plans, so you can share storage with your family +- Shared folders, in case you want your partner to enjoy your "Camera" clicks +- Album links, that can be protected with a password and set to expire +- Ability to free up space, by removing files that have been safely backed up +- Image editor, to add finishing touches +- Favorite, hide and relive your memories, for they are precious +- One-click import from all major storage providers +- Dark theme, because your photos look good in it +- 2FA, 3FA, biometric auth +- and a LOT more! + +PRICING +We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io. + +SUPPORT +We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours. + +TERMS +https://ente.io/terms diff --git a/mobile/fastlane/metadata/ios/sr/keywords.txt b/mobile/fastlane/metadata/ios/sr/keywords.txt new file mode 100644 index 0000000000..e1462baf51 --- /dev/null +++ b/mobile/fastlane/metadata/ios/sr/keywords.txt @@ -0,0 +1 @@ +photos,photography,family,privacy,cloud,backup,videos,photo,encryption,storage,album,alternative diff --git a/mobile/fastlane/metadata/ios/sr/name.txt b/mobile/fastlane/metadata/ios/sr/name.txt new file mode 100644 index 0000000000..3a991c4abc --- /dev/null +++ b/mobile/fastlane/metadata/ios/sr/name.txt @@ -0,0 +1 @@ +Ente Photos diff --git a/mobile/fastlane/metadata/ios/sr/subtitle.txt b/mobile/fastlane/metadata/ios/sr/subtitle.txt new file mode 100644 index 0000000000..958a35f1c9 --- /dev/null +++ b/mobile/fastlane/metadata/ios/sr/subtitle.txt @@ -0,0 +1 @@ +Encrypted photo storage diff --git a/mobile/fastlane/metadata/ios/tr/description.txt b/mobile/fastlane/metadata/ios/tr/description.txt index 9ee96f2770..9894ba4860 100644 --- a/mobile/fastlane/metadata/ios/tr/description.txt +++ b/mobile/fastlane/metadata/ios/tr/description.txt @@ -14,7 +14,7 @@ Ente, albümlerinizi sevdiklerinizle paylaşmanızı da kolaylaştırıyor. Bunl - Orijinal kalitede yedekler, çünkü her piksel önemlidir - Aile planları, böylece depolama alanını ailenizle paylaşabilirsiniz - Partnerinizin "Kamera" tıklamalarınızın keyfini çıkarmasını istemeniz durumunda paylaşılan klasörler -- Parola ile korunabilen ve süresi dolacak şekilde ayarlanabilen albüm bağlantıları +- Albüm bağlantıları, parolayla korunabilir ve son kullanma tarihi ayarlanabilir - Güvenli bir şekilde yedeklenmiş dosyaları kaldırarak alan boşaltma yeteneği - Son rötuşları eklemek için görüntü düzenleyici - Favori, sakla ve anılarını yeniden yaşa, çünkü onlar değerlidir @@ -26,7 +26,7 @@ Ente, albümlerinizi sevdiklerinizle paylaşmanızı da kolaylaştırıyor. Bunl FİYATLANDIRMA Sonsuza kadar ücretsiz planlar sunmuyoruz, çünkü sürdürülebilir kalmamız ve zamanın testine dayanmamız bizim için önemli. Bunun yerine, ailenizle özgürce paylaşabileceğiniz uygun fiyatlı planlar sunuyoruz. Daha fazla bilgiyi ente.io adresinde bulabilirsiniz. -🙋DESTEK +DESTEK İnsan desteği sunmaktan gurur duyuyoruz. Ücretli müşterimiz iseniz team@ente.io adresine ulaşabilir ve ekibimizden 24 saat içinde yanıt bekleyebilirsiniz. ŞARTLAR diff --git a/mobile/fastlane/metadata/ios/tr/subtitle.txt b/mobile/fastlane/metadata/ios/tr/subtitle.txt index e0b41db095..a9dc451727 100644 --- a/mobile/fastlane/metadata/ios/tr/subtitle.txt +++ b/mobile/fastlane/metadata/ios/tr/subtitle.txt @@ -1 +1 @@ -Ente - şifrelenmiş depolama sistemi +Ente şifrelenmiş depolama sistemi diff --git a/mobile/fastlane/metadata/playstore/sr/full_description.txt b/mobile/fastlane/metadata/playstore/sr/full_description.txt new file mode 100644 index 0000000000..ec999a783c --- /dev/null +++ b/mobile/fastlane/metadata/playstore/sr/full_description.txt @@ -0,0 +1,30 @@ +Ente is a simple app to automatically backup and organize your photos and videos. + +If you've been looking for a privacy-friendly alternative to preserve your memories, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. + +We have apps across Android, iOS, web and Desktop, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner. + +Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links. + +Your encrypted data is stored across multiple locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you. + +We are here to make the safest photos app ever, come join our journey! + +✨ FEATURES +- Original quality backups, because every pixel is important +- Family plans, so you can share storage with your family +- Shared folders, in case you want your partner to enjoy your "Camera" clicks +- Album links, that can be protected with a password and set to expire +- Ability to free up space, by removing files that have been safely backed up +- Image editor, to add finishing touches +- Favorite, hide and relive your memories, for they are precious +- One-click import from Google, Apple, your hard drive and more +- Dark theme, because your photos look good in it +- 2FA, 3FA, biometric auth +- and a LOT more! + +💲 PRICING +We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io. + +🙋 SUPPORT +We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours. \ No newline at end of file diff --git a/mobile/fastlane/metadata/playstore/sr/short_description.txt b/mobile/fastlane/metadata/playstore/sr/short_description.txt new file mode 100644 index 0000000000..6c00229894 --- /dev/null +++ b/mobile/fastlane/metadata/playstore/sr/short_description.txt @@ -0,0 +1 @@ +Encrypted photo storage - backup, organize and share your photos and videos \ No newline at end of file diff --git a/mobile/fastlane/metadata/playstore/sr/title.txt b/mobile/fastlane/metadata/playstore/sr/title.txt new file mode 100644 index 0000000000..97fdef3be7 --- /dev/null +++ b/mobile/fastlane/metadata/playstore/sr/title.txt @@ -0,0 +1 @@ +Ente Photos \ No newline at end of file diff --git a/mobile/fastlane/metadata/playstore/tr/full_description.txt b/mobile/fastlane/metadata/playstore/tr/full_description.txt index b13229830d..2f66882d32 100644 --- a/mobile/fastlane/metadata/playstore/tr/full_description.txt +++ b/mobile/fastlane/metadata/playstore/tr/full_description.txt @@ -14,7 +14,7 @@ Ente, albümlerinizi sevdiklerinizle paylaşmanızı da kolaylaştırıyor. Bunl - Orijinal kalitede yedekler, çünkü her piksel önemlidir - Aile planları, böylece depolama alanını ailenizle paylaşabilirsiniz - Partnerinizin "Kamera" tıklamalarınızın keyfini çıkarmasını istemeniz durumunda paylaşılan klasörler -- Parola ile korunabilen ve süresi dolacak şekilde ayarlanabilen albüm bağlantıları +- Albüm bağlantıları, parolayla korunabilir ve son kullanma tarihi ayarlanabilir - Güvenli bir şekilde yedeklenmiş dosyaları kaldırarak alan boşaltma yeteneği - Son rötuşları eklemek için görüntü düzenleyici - Favori, sakla ve anılarını yeniden yaşa, çünkü onlar değerlidir diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/AlbumsWidgetDefault.png b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/AlbumsWidgetDefault.png new file mode 100644 index 0000000000..7ffe666ae9 Binary files /dev/null and b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/AlbumsWidgetDefault.png differ diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/Contents.json new file mode 100644 index 0000000000..4ed24e6d76 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetDefault.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AlbumsWidgetDefault.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/AlbumsWidgetPreview.png b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/AlbumsWidgetPreview.png new file mode 100644 index 0000000000..a0ad77e39c Binary files /dev/null and b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/AlbumsWidgetPreview.png differ diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/Contents.json new file mode 100644 index 0000000000..de3cb3c542 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/AlbumsWidgetPreview.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AlbumsWidgetPreview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/IconGreen.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..c47b5f2f12 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift b/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift new file mode 100644 index 0000000000..c6712c7289 --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift @@ -0,0 +1,261 @@ +// +// EnteAlbumWidget.swift +// EnteAlbumWidget + +import SwiftUI +import UIKit +import WidgetKit + +private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" + +struct Provider: TimelineProvider { + let minutes = 15 + let data = UserDefaults(suiteName: widgetGroupId) + + func placeholder(in _: Context) -> FileEntry { + FileEntry( + date: Date(), index: nil, imageData: nil, title: "Title", subTitle: "Sub Title", + generatedId: nil, mainKey: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (FileEntry) -> Void) { + let entry = FileEntry( + date: Date(), index: -2, imageData: nil, title: "Favorites", + subTitle: "May 3, 2021", + generatedId: nil, mainKey: nil) + completion(entry) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + var entries: [FileEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Calendar.current.nextDate( + after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime, + direction: .backward + )! + + var totalAlbums = + data?.integer(forKey: "totalAlbums") + + if totalAlbums != nil && totalAlbums! > 0 { + let count = totalAlbums! > 5 ? 5 : totalAlbums + for offset in 0.. WidgetRelevances { + // // Generate a list containing the contexts this widget is relevant in. + // } +} + +struct FileEntry: TimelineEntry { + let date: Date + let index: Int? + let imageData: String? + let title: String? + let subTitle: String? + var generatedId: Int? + var mainKey: String? +} + +struct EnteAlbumWidgetEntryView: View { + var entry: Provider.Entry + let data = UserDefaults.init(suiteName: widgetGroupId) + + var body: some View { + GeometryReader { geometry in + ZStack { + if let imageData = entry.imageData, + let uiImage = UIImage(contentsOfFile: imageData) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } else if entry.index == -2 { + if let uiImage = UIImage(named: "AlbumsWidgetPreview") { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.7), Color.clear, + ]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } + } else if let uiImage = UIImage(named: "AlbumsWidgetDefault") { + VStack(spacing: 8) { + Spacer() + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fit) + .padding(8) + + Text("Go to Settings -> General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) + .foregroundStyle(.white) // Tint-aware color + .multilineTextAlignment(.center) + .padding(.bottom, 12) + .padding(.horizontal, 8) + .backwardWidgetAccentable(true) + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height) + } else { + Color.gray + } + } + .clipped() + .edgesIgnoringSafeArea(.all) + .widgetURL( + URL( + string: + "albumwidget://message?generatedId=\(entry.generatedId != nil ? String(entry.generatedId!) : "nan")&mainKey=\(entry.mainKey != nil ? entry.mainKey! : "nan")&homeWidget" + ) + ) + } + } +} + +struct EnteAlbumWidget: Widget { + let kind: String = "EnteAlbumWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + EnteAlbumWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + EnteAlbumWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("Albums") + .description("See photos from selected albums including your favorites") + .contentMarginsDisabled() + } +} + +#Preview(as: .systemSmall) { + EnteAlbumWidget() +} timeline: { + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) +} + +extension View { + @ViewBuilder + func backwardWidgetAccentable(_ accentable: Bool = true) -> some View { + if #available(iOS 16.0, *) { + self.widgetAccentable(accentable) + } else { + self + } + } +} + +extension Image { + @ViewBuilder + func backwardWidgetAccentedRenderingMode(_ isAccentedRenderingMode: Bool = true) -> some View { + if #available(iOS 18.0, *) { + self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor) + } else { + self + } + } + + @ViewBuilder + func backwardWidgetFullColorRenderingMode() -> some View { + backwardWidgetAccentedRenderingMode(false) + } +} diff --git a/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift b/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift new file mode 100644 index 0000000000..447511b8ae --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// EnteAlbumWidgetBundle.swift +// EnteAlbumWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@main +struct EnteAlbumWidgetBundle: WidgetBundle { + var body: some Widget { + EnteAlbumWidget() + } +} diff --git a/mobile/ios/EnteAlbumWidget/Info.plist b/mobile/ios/EnteAlbumWidget/Info.plist new file mode 100644 index 0000000000..0f118fb75e --- /dev/null +++ b/mobile/ios/EnteAlbumWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/mobile/ios/EnteAlbumWidgetExtension.entitlements b/mobile/ios/EnteAlbumWidgetExtension.entitlements new file mode 100644 index 0000000000..866b9602db --- /dev/null +++ b/mobile/ios/EnteAlbumWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.ente.frame.EnteMemoryWidget + + + diff --git a/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/Contents.json b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/Contents.json new file mode 100644 index 0000000000..6939b024f5 --- /dev/null +++ b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MemoriesWidgetDefault.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/MemoriesWidgetDefault.png b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/MemoriesWidgetDefault.png new file mode 100644 index 0000000000..616d6f3148 Binary files /dev/null and b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetDefault.imageset/MemoriesWidgetDefault.png differ diff --git a/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/Contents.json b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/Contents.json new file mode 100644 index 0000000000..b0162fa9b2 --- /dev/null +++ b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MemoriesWidgetPreview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/MemoriesWidgetPreview.png b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/MemoriesWidgetPreview.png new file mode 100644 index 0000000000..b32ee34731 Binary files /dev/null and b/mobile/ios/EnteMemoryWidget/Assets.xcassets/MemoriesWidgetPreview.imageset/MemoriesWidgetPreview.png differ diff --git a/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift b/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift index 0d7d3790e1..8a5e06abe8 100644 --- a/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift +++ b/mobile/ios/EnteMemoryWidget/EnteMemoryWidget.swift @@ -1,10 +1,6 @@ // // EnteMemoryWidget.swift // EnteMemoryWidget -// -// Created by Prateek Sunal on 3/7/25. -// Copyright © 2025 The Chromium Authors. All rights reserved. -// import SwiftUI import UIKit @@ -13,7 +9,7 @@ import WidgetKit private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" struct Provider: TimelineProvider { - let X = 15 + let minutes = 15 let data = UserDefaults(suiteName: widgetGroupId) func placeholder(in _: Context) -> FileEntry { @@ -47,7 +43,7 @@ struct Provider: TimelineProvider { for offset in 0.. General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) .foregroundStyle(.white) // Tint-aware color .multilineTextAlignment(.center) .padding(.bottom, 12) + .padding(.horizontal, 8) .backwardWidgetAccentable(true) Spacer() } @@ -229,8 +213,8 @@ struct EnteMemoryWidget: Widget { .background() } } - .configurationDisplayName("Your memories") - .description("See special moments from the past.") + .configurationDisplayName("Memories") + .description("See special moments from the past") .contentMarginsDisabled() } } diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/IconGreen.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/Contents.json new file mode 100644 index 0000000000..69d59a1081 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "PeopleWidgetDefault.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/PeopleWidgetDefault.png b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/PeopleWidgetDefault.png new file mode 100644 index 0000000000..f935479c04 Binary files /dev/null and b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetDefault.imageset/PeopleWidgetDefault.png differ diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/Contents.json new file mode 100644 index 0000000000..47b392a1ff --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "PeopleWidgetPreview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/PeopleWidgetPreview.png b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/PeopleWidgetPreview.png new file mode 100644 index 0000000000..b2e27c654c Binary files /dev/null and b/mobile/ios/EntePeopleWidget/Assets.xcassets/PeopleWidgetPreview.imageset/PeopleWidgetPreview.png differ diff --git a/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift b/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift new file mode 100644 index 0000000000..35537c5c1a --- /dev/null +++ b/mobile/ios/EntePeopleWidget/EntePeopleWidget.swift @@ -0,0 +1,261 @@ +// +// EntePeopleWidget.swift +// EntePeopleWidget + +import SwiftUI +import UIKit +import WidgetKit + +private let widgetGroupId = "group.io.ente.frame.EnteMemoryWidget" + +struct Provider: TimelineProvider { + let minutes = 15 + let data = UserDefaults(suiteName: widgetGroupId) + + func placeholder(in _: Context) -> FileEntry { + FileEntry( + date: Date(), index: nil, imageData: nil, title: "Title", subTitle: "Sub Title", + generatedId: nil, mainKey: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (FileEntry) -> Void) { + let entry = FileEntry( + date: Date(), index: -2, imageData: nil, title: "Jane Fonda", + subTitle: "Sep 23, 2021", + generatedId: nil, mainKey: nil) + completion(entry) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + var entries: [FileEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Calendar.current.nextDate( + after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime, + direction: .backward + )! + + var totalPeople = + data?.integer(forKey: "totalPeople") + + if totalPeople != nil && totalPeople! > 0 { + let count = totalPeople! > 5 ? 5 : totalPeople + for offset in 0.. WidgetRelevances { + // // Generate a list containing the contexts this widget is relevant in. + // } +} + +struct FileEntry: TimelineEntry { + let date: Date + let index: Int? + let imageData: String? + let title: String? + let subTitle: String? + var generatedId: Int? + var mainKey: String? +} + +struct EntePeopleWidgetEntryView: View { + var entry: Provider.Entry + let data = UserDefaults.init(suiteName: widgetGroupId) + + var body: some View { + GeometryReader { geometry in + ZStack { + if let imageData = entry.imageData, + let uiImage = UIImage(contentsOfFile: imageData) + { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [Color.black.opacity(0.7), Color.clear]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } else if entry.index == -2 { + if let uiImage = UIImage(named: "PeopleWidgetPreview") { + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: geometry.size.height) + .overlay( + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.7), Color.clear, + ]), + startPoint: .bottom, + endPoint: .top + ) + + .frame(height: geometry.size.height * 0.4) + .frame(maxHeight: .infinity, alignment: .bottom) + .backwardWidgetAccentable(true) + ) + .overlay( + VStack(alignment: .leading, spacing: 2) { + Text(entry.title ?? "").font( + .custom("Inter", size: 14, relativeTo: .caption) + ) // Custom with fallback + .bold() + .foregroundStyle(.white) + .shadow(radius: 20) + Text(entry.subTitle ?? "") + .font(.custom("Inter", size: 12, relativeTo: .caption2)) + .foregroundStyle(.white) + .shadow(radius: 20) + } + .padding(.leading, geometry.size.width * 0.05) + .padding(.bottom, geometry.size.height * 0.05), + alignment: .bottomLeading + ) + } + } else if let uiImage = UIImage(named: "PeopleWidgetDefault") { + VStack(spacing: 8) { + Spacer() + Image(uiImage: uiImage) + .resizable() + .backwardWidgetFullColorRenderingMode() + .aspectRatio(contentMode: .fit) + .padding(8) + + Text("Go to Settings -> General to customise the widget") + .font(.custom("Inter", size: 12, relativeTo: .caption)) + .foregroundStyle(.white) // Tint-aware color + .multilineTextAlignment(.center) + .padding(.bottom, 12) + .padding(.horizontal, 8) + .backwardWidgetAccentable(true) + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height) + } else { + Color.gray + } + } + .clipped() + .edgesIgnoringSafeArea(.all) + .widgetURL( + URL( + string: + "peoplewidget://message?generatedId=\(entry.generatedId != nil ? String(entry.generatedId!) : "nan")&mainKey=\(entry.mainKey != nil ? entry.mainKey! : "nan")&homeWidget" + ) + ) + } + } +} + +struct EntePeopleWidget: Widget { + let kind: String = "EntePeopleWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + EntePeopleWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + EntePeopleWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("People Widget") + .description("See photos of your favorite people") + .contentMarginsDisabled() + } +} + +#Preview(as: .systemSmall) { + EntePeopleWidget() +} timeline: { + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) + FileEntry( + date: .now, index: -2, imageData: nil, title: nil, subTitle: nil, generatedId: nil, + mainKey: nil) +} + +extension View { + @ViewBuilder + func backwardWidgetAccentable(_ accentable: Bool = true) -> some View { + if #available(iOS 16.0, *) { + self.widgetAccentable(accentable) + } else { + self + } + } +} + +extension Image { + @ViewBuilder + func backwardWidgetAccentedRenderingMode(_ isAccentedRenderingMode: Bool = true) -> some View { + if #available(iOS 18.0, *) { + self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor) + } else { + self + } + } + + @ViewBuilder + func backwardWidgetFullColorRenderingMode() -> some View { + backwardWidgetAccentedRenderingMode(false) + } +} diff --git a/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift b/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift new file mode 100644 index 0000000000..e68b752d45 --- /dev/null +++ b/mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// EntePeopleWidgetBundle.swift +// EntePeopleWidget +// +// Created by Prateek Sunal on 5/15/25. +// Copyright © 2025 The Chromium Authors. All rights reserved. +// + +import WidgetKit +import SwiftUI + +@main +struct EntePeopleWidgetBundle: WidgetBundle { + var body: some Widget { + EntePeopleWidget() + } +} diff --git a/mobile/ios/EntePeopleWidget/Info.plist b/mobile/ios/EntePeopleWidget/Info.plist new file mode 100644 index 0000000000..0f118fb75e --- /dev/null +++ b/mobile/ios/EntePeopleWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/mobile/ios/EntePeopleWidgetExtension.entitlements b/mobile/ios/EntePeopleWidgetExtension.entitlements new file mode 100644 index 0000000000..866b9602db --- /dev/null +++ b/mobile/ios/EntePeopleWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.ente.frame.EnteMemoryWidget + + + diff --git a/mobile/ios/Flutter/ephemeral/flutter_lldb_helper.py b/mobile/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000000..a88caf99df --- /dev/null +++ b/mobile/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/mobile/ios/Flutter/ephemeral/flutter_lldbinit b/mobile/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000000..e3ba6fbedc --- /dev/null +++ b/mobile/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index d76dd0c63d..a72d5beab2 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - app_links (0.0.2): - Flutter - - background_fetch (1.3.7): + - background_fetch (1.3.8): - Flutter - battery_info (0.0.1): - Flutter @@ -20,31 +20,31 @@ PODS: - Flutter - file_saver (0.0.1): - Flutter - - Firebase/CoreOnly (11.8.0): - - FirebaseCore (~> 11.8.0) - - Firebase/Messaging (11.8.0): + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - Firebase/Messaging (11.10.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 11.8.0) - - firebase_core (3.12.0): - - Firebase/CoreOnly (= 11.8.0) + - FirebaseMessaging (~> 11.10.0) + - firebase_core (3.13.1): + - Firebase/CoreOnly (= 11.10.0) - Flutter - - firebase_messaging (15.2.3): - - Firebase/Messaging (= 11.8.0) + - firebase_messaging (15.2.6): + - Firebase/Messaging (= 11.10.0) - firebase_core - Flutter - - FirebaseCore (11.8.1): - - FirebaseCoreInternal (~> 11.8.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreInternal (11.8.0): + - FirebaseCoreInternal (11.10.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseInstallations (11.8.0): - - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (11.10.0): + - FirebaseCore (~> 11.10.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (11.8.0): - - FirebaseCore (~> 11.8.0) + - FirebaseMessaging (11.10.0): + - FirebaseCore (~> 11.10.0) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -75,33 +75,35 @@ PODS: - Flutter - flutter_sodium (0.0.1): - Flutter + - flutter_timezone (0.0.1): + - Flutter - fluttertoast (0.0.2): - Flutter - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.0.2)": + - "GoogleUtilities/NSData+zlib (8.1.0)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.0.2) - - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - home_widget (0.0.1): @@ -127,9 +129,6 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - local_auth_darwin (0.0.1): - - Flutter - - FlutterMacOS - local_auth_ios (0.0.1): - Flutter - Mantle (2.2.0): @@ -154,7 +153,7 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - native_video_player (1.0.0): + - native_video_player (4.0.0): - Flutter - objective_c (0.0.1): - Flutter @@ -176,7 +175,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - photo_manager (2.0.0): + - photo_manager (3.7.1): - Flutter - FlutterMacOS - privacy_screen (0.0.1): @@ -191,7 +190,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - Sentry/HybridSDK (8.46.0) - - sentry_flutter (8.14.1): + - sentry_flutter (8.14.2): - Flutter - FlutterMacOS - Sentry/HybridSDK (= 8.46.0) @@ -203,27 +202,32 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.49.0) + - sqlite3 (~> 3.49.2) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - system_info_plus (0.0.1): - Flutter + - thermal (0.0.1): + - Flutter - ua_client_hints (1.4.1): - Flutter - url_launcher_ios (0.0.1): @@ -259,13 +263,13 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`) - - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - maps_launcher (from `.symlinks/plugins/maps_launcher/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) @@ -290,6 +294,7 @@ DEPENDENCIES: - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - system_info_plus (from `.symlinks/plugins/system_info_plus/ios`) + - thermal (from `.symlinks/plugins/thermal/ios`) - ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -359,6 +364,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_sodium: :path: ".symlinks/plugins/flutter_sodium/ios" + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" home_widget: @@ -371,8 +378,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" launcher_icon_switcher: :path: ".symlinks/plugins/launcher_icon_switcher/ios" - local_auth_darwin: - :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" maps_launcher: @@ -421,6 +426,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" + thermal: + :path: ".symlinks/plugins/thermal/ios" ua_client_hints: :path: ".symlinks/plugins/ua_client_hints/ios" url_launcher_ios: @@ -436,40 +443,40 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 - background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57 + background_fetch: 851122c99dc3f25a011a6aebec5379ccdf4ab5eb battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99 ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 - Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf - firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f - firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac - FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d - FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 - FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 - FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad + firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067 + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 + FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3 + FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1 in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45 @@ -480,7 +487,7 @@ SPEC CHECKSUMS: motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1 move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13 + native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c @@ -490,26 +497,27 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 + photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 - sentry_flutter: 942017adbe00f963061cb11ec260414a990b7a42 + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4 system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 + thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41 ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620 volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 - wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 678d1e3446..caaa39afbc 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,12 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + CEE166242DD5E7820012CF61 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; + CEE166252DD5E7820012CF61 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; + CEE166302DD5E7830012CF61 /* EnteAlbumWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CEE1667C2DD5F6F20012CF61 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; + CEE1667D2DD5F6F20012CF61 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; + CEE166882DD5F6F30012CF61 /* EntePeopleWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; CEE6BE702D7AE7FD00E4048B /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83C2B755B0600BA9516 /* WidgetKit.framework */; }; CEE6BE712D7AE7FD00E4048B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DACD83E2B755B0600BA9516 /* SwiftUI.framework */; }; CEE6BE7C2D7AE7FE00E4048B /* EnteMemoryWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -23,6 +29,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + CEE1662E2DD5E7830012CF61 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE166222DD5E7820012CF61; + remoteInfo = EnteAlbumWidgetExtension; + }; + CEE166862DD5F6F30012CF61 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE1667A2DD5F6F20012CF61; + remoteInfo = EntePeopleWidgetExtension; + }; CEE6BE7A2D7AE7FE00E4048B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -39,7 +59,9 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + CEE166302DD5E7830012CF61 /* EnteAlbumWidgetExtension.appex in Embed Foundation Extensions */, CEE6BE7C2D7AE7FE00E4048B /* EnteMemoryWidgetExtension.appex in Embed Foundation Extensions */, + CEE166882DD5F6F30012CF61 /* EntePeopleWidgetExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -79,6 +101,10 @@ A78E51A260432466D4C456A9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; BB097BB5EB0EEB41344338D2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; CE93A9062D808893005CD942 /* EnteMemoryWidgetExtensionDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteMemoryWidgetExtensionDebug.entitlements; sourceTree = ""; }; + CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EnteAlbumWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE166362DD5E7950012CF61 /* EnteAlbumWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteAlbumWidgetExtension.entitlements; sourceTree = ""; }; + CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EntePeopleWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE1668E2DD5F70F0012CF61 /* EntePeopleWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EntePeopleWidgetExtension.entitlements; sourceTree = ""; }; CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EnteMemoryWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; CEE6BE822D7AE8C700E4048B /* EnteMemoryWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EnteMemoryWidgetExtension.entitlements; sourceTree = ""; }; DA8D6672273BBB59007651D4 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; @@ -86,6 +112,20 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + CEE166342DD5E7830012CF61 /* Exceptions for "EnteAlbumWidget" folder in "EnteAlbumWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */; + }; + CEE1668C2DD5F6F30012CF61 /* Exceptions for "EntePeopleWidget" folder in "EntePeopleWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */; + }; CEE6BE802D7AE7FE00E4048B /* Exceptions for "EnteMemoryWidget" folder in "EnteMemoryWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -96,6 +136,30 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + CEE166342DD5E7830012CF61 /* Exceptions for "EnteAlbumWidget" folder in "EnteAlbumWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = EnteAlbumWidget; + sourceTree = ""; + }; + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + CEE1668C2DD5F6F30012CF61 /* Exceptions for "EntePeopleWidget" folder in "EntePeopleWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = EntePeopleWidget; + sourceTree = ""; + }; CEE6BE722D7AE7FD00E4048B /* EnteMemoryWidget */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -120,6 +184,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE166202DD5E7820012CF61 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CEE166252DD5E7820012CF61 /* SwiftUI.framework in Frameworks */, + CEE166242DD5E7820012CF61 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166782DD5F6F20012CF61 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CEE1667D2DD5F6F20012CF61 /* SwiftUI.framework in Frameworks */, + CEE1667C2DD5F6F20012CF61 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6C2D7AE7FD00E4048B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -146,12 +228,16 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + CEE1668E2DD5F70F0012CF61 /* EntePeopleWidgetExtension.entitlements */, + CEE166362DD5E7950012CF61 /* EnteAlbumWidgetExtension.entitlements */, CE93A9062D808893005CD942 /* EnteMemoryWidgetExtensionDebug.entitlements */, CEE6BE822D7AE8C700E4048B /* EnteMemoryWidgetExtension.entitlements */, 2772189F270F596900FFE3CC /* GoogleService-Info.plist */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, CEE6BE722D7AE7FD00E4048B /* EnteMemoryWidget */, + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */, + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */, 97C146EF1CF9000F007C117D /* Products */, AC6CA265BB505D982CB00391 /* Pods */, C6A22658E77FF012720BEDDA /* Frameworks */, @@ -163,6 +249,8 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, CEE6BE6F2D7AE7FD00E4048B /* EnteMemoryWidgetExtension.appex */, + CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */, + CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -235,12 +323,54 @@ ); dependencies = ( CEE6BE7B2D7AE7FE00E4048B /* PBXTargetDependency */, + CEE1662F2DD5E7830012CF61 /* PBXTargetDependency */, + CEE166872DD5F6F30012CF61 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE166352DD5E7830012CF61 /* Build configuration list for PBXNativeTarget "EnteAlbumWidgetExtension" */; + buildPhases = ( + CEE1661F2DD5E7820012CF61 /* Sources */, + CEE166202DD5E7820012CF61 /* Frameworks */, + CEE166212DD5E7820012CF61 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + CEE166262DD5E7820012CF61 /* EnteAlbumWidget */, + ); + name = EnteAlbumWidgetExtension; + productName = EnteAlbumWidgetExtension; + productReference = CEE166232DD5E7820012CF61 /* EnteAlbumWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE1668D2DD5F6F30012CF61 /* Build configuration list for PBXNativeTarget "EntePeopleWidgetExtension" */; + buildPhases = ( + CEE166772DD5F6F20012CF61 /* Sources */, + CEE166782DD5F6F20012CF61 /* Frameworks */, + CEE166792DD5F6F20012CF61 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + CEE1667E2DD5F6F20012CF61 /* EntePeopleWidget */, + ); + name = EntePeopleWidgetExtension; + productName = EntePeopleWidgetExtension; + productReference = CEE1667B2DD5F6F20012CF61 /* EntePeopleWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */ = { isa = PBXNativeTarget; buildConfigurationList = CEE6BE812D7AE7FE00E4048B /* Build configuration list for PBXNativeTarget "EnteMemoryWidgetExtension" */; @@ -267,7 +397,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 1630; LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { @@ -276,6 +406,12 @@ LastSwiftMigration = 1100; ProvisioningStyle = Automatic; }; + CEE166222DD5E7820012CF61 = { + CreatedOnToolsVersion = 16.3; + }; + CEE1667A2DD5F6F20012CF61 = { + CreatedOnToolsVersion = 16.3; + }; CEE6BE6E2D7AE7FD00E4048B = { CreatedOnToolsVersion = 16.2; }; @@ -296,6 +432,8 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */, + CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */, + CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */, ); }; /* End PBXProject section */ @@ -314,6 +452,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE166212DD5E7820012CF61 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166792DD5F6F20012CF61 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6D2D7AE7FD00E4048B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -389,6 +541,7 @@ "${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/flutter_sodium/flutter_sodium.framework", + "${BUILT_PRODUCTS_DIR}/flutter_timezone/flutter_timezone.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", "${BUILT_PRODUCTS_DIR}/home_widget/home_widget.framework", "${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework", @@ -396,7 +549,6 @@ "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", - "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", "${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", @@ -421,6 +573,7 @@ "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/system_info_plus/system_info_plus.framework", + "${BUILT_PRODUCTS_DIR}/thermal/thermal.framework", "${BUILT_PRODUCTS_DIR}/ua_client_hints/ua_client_hints.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", @@ -483,6 +636,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_sodium.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_timezone.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/home_widget.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework", @@ -490,7 +644,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", @@ -515,6 +668,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/system_info_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/thermal.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ua_client_hints.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", @@ -607,6 +761,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE1661F2DD5E7820012CF61 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CEE166772DD5F6F20012CF61 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CEE6BE6B2D7AE7FD00E4048B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -617,6 +785,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + CEE1662F2DD5E7830012CF61 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE166222DD5E7820012CF61 /* EnteAlbumWidgetExtension */; + targetProxy = CEE1662E2DD5E7830012CF61 /* PBXContainerItemProxy */; + }; + CEE166872DD5F6F30012CF61 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE1667A2DD5F6F20012CF61 /* EntePeopleWidgetExtension */; + targetProxy = CEE166862DD5F6F30012CF61 /* PBXContainerItemProxy */; + }; CEE6BE7B2D7AE7FE00E4048B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CEE6BE6E2D7AE7FD00E4048B /* EnteMemoryWidgetExtension */; @@ -949,6 +1127,250 @@ }; name = Release; }; + CEE166312DD5E7830012CF61 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.debug.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE166322DD5E7830012CF61 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CEE166332DD5E7830012CF61 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EnteAlbumWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EnteAlbumWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EnteAlbumWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EnteAlbumWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + CEE166892DD5F6F30012CF61 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.debug.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE1668A2DD5F6F30012CF61 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CEE1668B2DD5F6F30012CF61 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = EntePeopleWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6Z68YJY9Q2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EntePeopleWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = EntePeopleWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.frame.EntePeopleWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; CEE6BE7D2D7AE7FE00E4048B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1094,6 +1516,26 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEE166352DD5E7830012CF61 /* Build configuration list for PBXNativeTarget "EnteAlbumWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE166312DD5E7830012CF61 /* Debug */, + CEE166322DD5E7830012CF61 /* Release */, + CEE166332DD5E7830012CF61 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CEE1668D2DD5F6F30012CF61 /* Build configuration list for PBXNativeTarget "EntePeopleWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE166892DD5F6F30012CF61 /* Debug */, + CEE1668A2DD5F6F30012CF61 /* Release */, + CEE1668B2DD5F6F30012CF61 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CEE6BE812D7AE7FE00E4048B /* Build configuration list for PBXNativeTarget "EnteMemoryWidgetExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d342..9c12df59c6 100644 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/mobile/l10n.yaml b/mobile/l10n.yaml deleted file mode 100644 index b522046b57..0000000000 --- a/mobile/l10n.yaml +++ /dev/null @@ -1,3 +0,0 @@ -arb-dir: lib/l10n -template-arb-file: intl_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 3b8e3346d8..3f309bd886 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -6,23 +6,26 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter_localizations/flutter_localizations.dart"; import 'package:home_widget/home_widget.dart' as hw; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension_action_types.dart'; import "package:photos/core/event_bus.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/events/memories_changed_event.dart"; +import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/home_widget_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; +import "package:photos/services/people_home_widget_service.dart"; import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/ui/tabs/home_widget.dart'; import "package:photos/ui/viewer/actions/file_viewer.dart"; import "package:photos/utils/intent_util.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class EnteApp extends StatefulWidget { final Future Function(String) runBackgroundTask; @@ -51,6 +54,8 @@ class _EnteAppState extends State with WidgetsBindingObserver { final _logger = Logger("EnteAppState"); late Locale? locale; late StreamSubscription _memoriesChangedSubscription; + late StreamSubscription _peopleChangedSubscription; + late Debouncer _changeCallbackDebouncer; @override void initState() { @@ -69,6 +74,15 @@ class _EnteAppState extends State with WidgetsBindingObserver { await MemoryHomeWidgetService.instance.memoryChanged(); }, ); + _changeCallbackDebouncer = Debouncer(const Duration(milliseconds: 1500)); + _peopleChangedSubscription = Bus.instance.on().listen( + (event) async { + _changeCallbackDebouncer.run( + () async => + unawaited(PeopleHomeWidgetService.instance.peopleChanged()), + ); + }, + ); } @override @@ -107,7 +121,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { if (Platform.isAndroid || kDebugMode) { return Listener( onPointerDown: (event) { - machineLearningController.onUserInteraction(); + computeController.onUserInteraction(); }, child: AdaptiveTheme( light: lightThemeData, @@ -133,8 +147,10 @@ class _EnteAppState extends State with WidgetsBindingObserver { supportedLocales: appSupportedLocales, localeListResolutionCallback: localResolutionCallBack, localizationsDelegates: const [ - ...AppLocalizations.localizationsDelegates, S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, ], ), ), @@ -142,7 +158,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { } else { return Listener( onPointerDown: (event) { - machineLearningController.onUserInteraction(); + computeController.onUserInteraction(); }, child: MaterialApp( title: "ente", @@ -156,8 +172,10 @@ class _EnteAppState extends State with WidgetsBindingObserver { supportedLocales: appSupportedLocales, localeListResolutionCallback: localResolutionCallBack, localizationsDelegates: const [ - ...AppLocalizations.localizationsDelegates, S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, ], ), ); @@ -168,6 +186,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); _memoriesChangedSubscription.cancel(); + _peopleChangedSubscription.cancel(); super.dispose(); } diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index 78f3db6bad..6215ebc7d7 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -71,7 +71,7 @@ const kSearchSectionLimit = 9; const maxPickAssetLimit = 50; -const iOSGroupID = "group.io.ente.frame.EnteMemoryWidget"; +const iOSGroupIDMemory = "group.io.ente.frame.EnteMemoryWidget"; const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' @@ -101,12 +101,9 @@ const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' 'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' 'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k='; -const localFileServer = - String.fromEnvironment("localFileServer", defaultValue: ""); - const uploadTempFilePrefix = "upload_file_"; final tempDirCleanUpInterval = kDebugMode - ? const Duration(seconds: 30).inMicroseconds + ? const Duration(hours: 1).inMicroseconds : const Duration(hours: 6).inMicroseconds; const kFilterChipHeight = 32.0; diff --git a/mobile/lib/core/error-reporting/super_logging.dart b/mobile/lib/core/error-reporting/super_logging.dart index 31cd3d9137..50950d342f 100644 --- a/mobile/lib/core/error-reporting/super_logging.dart +++ b/mobile/lib/core/error-reporting/super_logging.dart @@ -1,5 +1,3 @@ -library super_logging; - import 'dart:async'; import 'dart:collection'; import 'dart:core'; diff --git a/mobile/lib/core/network/network.dart b/mobile/lib/core/network/network.dart index ba2540b401..2179dc7850 100644 --- a/mobile/lib/core/network/network.dart +++ b/mobile/lib/core/network/network.dart @@ -39,7 +39,6 @@ class NetworkClient { ), ); - _dio.httpClientAdapter = NativeAdapter(); _enteDio.httpClientAdapter = NativeAdapter(); _setupInterceptors(endpoint); diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index ae702d92b4..1d46bff519 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -640,7 +640,6 @@ class FilesDB with SqlDbBase { int visibility = visibleVisibility, DBFilterOptions? filterOptions, bool applyOwnerCheck = false, - bool ignoreSharedFiles = false, }) async { final stopWatch = EnteWatch('getAllPendingOrUploadedFiles')..start(); final order = (asc ?? false ? 'ASC' : 'DESC'); @@ -663,7 +662,7 @@ class FilesDB with SqlDbBase { subQueries.add(' AND $columnMMdVisibility = ?'); args.add(visibility); - if (ignoreSharedFiles == true) { + if (filterOptions?.ignoreSharedItems ?? false) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } @@ -696,7 +695,6 @@ class FilesDB with SqlDbBase { int ownerID, { int? limit, bool? asc, - bool ignoreSharedFiles = false, required DBFilterOptions filterOptions, }) async { final db = await instance.sqliteAsyncDB; @@ -708,7 +706,7 @@ class FilesDB with SqlDbBase { 'SELECT * FROM $filesTable WHERE $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)' ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))'); - if (ignoreSharedFiles == true) { + if (filterOptions.ignoreSharedItems) { subQueries.add(' AND $columnOwnerID = ?'); args.add(ownerID); } @@ -1685,8 +1683,8 @@ class FilesDB with SqlDbBase { AND $columnUploadedFileID != -1 AND $columnOwnerID = $userID AND $columnLocalID IS NOT NULL - AND ($columnFileSize IS NULL OR $columnFileSize <= 524288000) - AND ($columnDuration IS NULL OR $columnDuration <= 60) + AND ($columnFileSize IS NOT NULL AND $columnFileSize <= 524288000) + AND ($columnDuration IS NOT NULL AND ($columnDuration <= 60 AND $columnDuration > 0)) ORDER BY $columnCreationTime DESC ''', [getInt(fileType), beginDate.microsecondsSinceEpoch], diff --git a/mobile/lib/db/ml/db.dart b/mobile/lib/db/ml/db.dart index 0186ead274..a1d2a6ccf1 100644 --- a/mobile/lib/db/ml/db.dart +++ b/mobile/lib/db/ml/db.dart @@ -35,6 +35,8 @@ import 'package:sqlite_async/sqlite_async.dart'; /// /// [clipTable] - Stores the embeddings of the CLIP model /// [fileDataTable] - Stores data about the files that are already processed by the ML models +/// +/// [faceCacheTable] - Stores a all the mappings from personID or clusterID to the faceID that has been used as cover face. class MLDataDB with SqlDbBase implements IMLDataDB { static final Logger _logger = Logger("MLDataDB"); @@ -57,6 +59,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB { fcClusterIDIndex, createClipEmbeddingsTable, createFileDataTable, + createFaceCacheTable, ]; // only have a single app-wide reference to the database @@ -471,6 +474,22 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return maps.map((e) => e[faceIDColumn] as String).toSet(); } + Future> getFaceIDsForClusterOrderedByScore( + String clusterID, { + int limit = 10, + }) async { + final db = await instance.asyncDB; + final faceIdsResult = await db.getAll( + 'SELECT $facesTable.$faceIDColumn FROM $facesTable ' + 'JOIN $faceClustersTable ON $facesTable.$faceIDColumn = $faceClustersTable.$faceIDColumn ' + 'WHERE $faceClustersTable.$clusterIDColumn = ? ' + 'ORDER BY $facesTable.$faceScore DESC ' + 'LIMIT ?', + [clusterID, limit], + ); + return faceIdsResult.map((e) => e[faceIDColumn] as String).toList(); + } + // Get Map of personID to Map of clusterID to faceIDs @override Future>>> @@ -541,6 +560,23 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return faceIdsResult.map((e) => e[faceIDColumn] as String).toSet(); } + Future> getFaceIDsForPersonOrderedByScore( + String personID, { + int limit = 10, + }) async { + final db = await instance.asyncDB; + final faceIdsResult = await db.getAll( + 'SELECT $facesTable.$faceIDColumn FROM $facesTable ' + 'JOIN $faceClustersTable ON $facesTable.$faceIDColumn = $faceClustersTable.$faceIDColumn ' + 'JOIN $clusterPersonTable ON $faceClustersTable.$clusterIDColumn = $clusterPersonTable.$clusterIDColumn ' + 'WHERE $clusterPersonTable.$personIdColumn = ? ' + 'ORDER BY $facesTable.$faceScore DESC ' + 'LIMIT ?', + [personID, limit], + ); + return faceIdsResult.map((e) => e[faceIDColumn] as String).toList(); + } + @override Future> getBlurValuesForCluster(String clusterID) async { final db = await instance.asyncDB; @@ -1249,4 +1285,49 @@ class MLDataDB with SqlDbBase implements IMLDataDB { embedding.version, ]; } + + /// WARNING: Better to use the similarly named [putFaceIdCachedForPersonOrCluster] + /// method from face_thumbnail_cache instead! + Future putFaceIdCachedForPersonOrCluster( + String personOrClusterId, + String faceID, + ) async { + final db = await instance.asyncDB; + await db.execute( + ''' + INSERT OR REPLACE INTO $faceCacheTable ($personOrClusterIdColumn, $faceIDColumn) + VALUES (?, ?) + ''', + [personOrClusterId, faceID], + ); + } + + Future getFaceIdUsedForPersonOrCluster( + String personOrClusterId, + ) async { + final db = await instance.asyncDB; + final List> maps = await db.getAll( + ''' + SELECT $faceIDColumn FROM $faceCacheTable + WHERE $personOrClusterIdColumn = ? + ''', + [personOrClusterId], + ); + if (maps.isNotEmpty) { + return maps.first[faceIDColumn] as String; + } + return null; + } + + Future removeFaceIdCachedForPersonOrCluster( + String personOrClusterID, + ) async { + final db = await instance.asyncDB; + const String sql = ''' + DELETE FROM $faceCacheTable + WHERE $personOrClusterIdColumn = ?' + '''; + final List params = [personOrClusterID]; + await db.execute(sql, params); + } } diff --git a/mobile/lib/db/ml/schema.dart b/mobile/lib/db/ml/schema.dart index 5968215cf5..d2306cd5d2 100644 --- a/mobile/lib/db/ml/schema.dart +++ b/mobile/lib/db/ml/schema.dart @@ -15,6 +15,7 @@ const imageHeight = 'height'; const mlVersionColumn = 'ml_version'; const personIdColumn = 'person_id'; const clusterIDColumn = 'cluster_id'; +const personOrClusterIdColumn = 'person_or_cluster_id'; const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable ( $fileIDColumn INTEGER NOT NULL, @@ -123,3 +124,16 @@ CREATE TABLE IF NOT EXISTS $fileDataTable ( '''; const deleteFileDataTable = 'DELETE FROM $fileDataTable'; + +// ## FACE CACHE TABLE +const faceCacheTable = 'face_cache'; + +const createFaceCacheTable = ''' +CREATE TABLE IF NOT EXISTS $faceCacheTable ( + $personOrClusterIdColumn TEXT NOT NULL UNIQUE, + $faceIDColumn TEXT NOT NULL UNIQUE, + PRIMARY KEY ($personOrClusterIdColumn) +); +'''; + +const deleteFaceCacheTable = 'DELETE FROM $faceCacheTable'; diff --git a/mobile/lib/ente_theme_data.dart b/mobile/lib/ente_theme_data.dart index c7ed51640b..3bfc85a4e3 100644 --- a/mobile/lib/ente_theme_data.dart +++ b/mobile/lib/ente_theme_data.dart @@ -42,20 +42,22 @@ final lightThemeData = ThemeData( bodyLarge: const TextStyle(color: Colors.orange), ), cardColor: const Color.fromRGBO(250, 250, 250, 1.0), - dialogTheme: const DialogTheme().copyWith( - backgroundColor: const Color.fromRGBO(250, 250, 250, 1.0), // - titleTextStyle: const TextStyle( + dialogTheme: const DialogThemeData( + backgroundColor: Color.fromRGBO(250, 250, 250, 1.0), // + titleTextStyle: TextStyle( color: Colors.black, fontSize: 24, fontWeight: FontWeight.w600, ), - contentTextStyle: const TextStyle( + contentTextStyle: TextStyle( fontFamily: 'Inter-Medium', color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500, ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), ), inputDecorationTheme: const InputDecorationTheme().copyWith( focusedBorder: const UnderlineInputBorder( @@ -117,20 +119,22 @@ final darkThemeData = ThemeData( elevation: 0, ), cardColor: const Color.fromRGBO(10, 15, 15, 1.0), - dialogTheme: const DialogTheme().copyWith( - backgroundColor: const Color.fromRGBO(15, 15, 15, 1.0), - titleTextStyle: const TextStyle( + dialogTheme: const DialogThemeData( + backgroundColor: Color.fromRGBO(15, 15, 15, 1.0), + titleTextStyle: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.w600, ), - contentTextStyle: const TextStyle( + contentTextStyle: TextStyle( fontFamily: 'Inter-Medium', color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), ), inputDecorationTheme: const InputDecorationTheme().copyWith( focusedBorder: const UnderlineInputBorder( @@ -206,7 +210,7 @@ TextTheme _buildTextTheme(Color textColor) { fontWeight: FontWeight.w500, ), bodySmall: TextStyle( - color: textColor.withOpacity(0.6), + color: textColor.withValues(alpha: 0.6), fontSize: 14, fontWeight: FontWeight.w500, ), @@ -278,7 +282,7 @@ extension CustomColorScheme on ColorScheme { : const Color.fromRGBO(48, 48, 48, 0.5); Color get iconColor => brightness == Brightness.light - ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.75) + ? const Color.fromRGBO(0, 0, 0, 1).withValues(alpha: 0.75) : const Color.fromRGBO(255, 255, 255, 1); Color get bgColorForQuestions => brightness == Brightness.light @@ -289,7 +293,7 @@ extension CustomColorScheme on ColorScheme { Color get cupertinoPickerTopColor => brightness == Brightness.light ? const Color.fromARGB(255, 238, 238, 238) - : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.1); + : const Color.fromRGBO(255, 255, 255, 1).withValues(alpha: 0.1); Color get stepProgressUnselectedColor => brightness == Brightness.light ? const Color.fromRGBO(196, 196, 196, 0.6) @@ -300,20 +304,20 @@ extension CustomColorScheme on ColorScheme { : const Color.fromRGBO(20, 20, 20, 1); Color get galleryThumbDrawColor => brightness == Brightness.light - ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.8) - : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5); + ? const Color.fromRGBO(0, 0, 0, 1).withValues(alpha: 0.8) + : const Color.fromRGBO(255, 255, 255, 1).withValues(alpha: 0.5); Color get backupEnabledBgColor => brightness == Brightness.light ? const Color.fromRGBO(230, 230, 230, 0.95) : const Color.fromRGBO(10, 40, 40, 0.3); Color get dotsIndicatorActiveColor => brightness == Brightness.light - ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.5) - : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5); + ? const Color.fromRGBO(0, 0, 0, 1).withValues(alpha: 0.5) + : const Color.fromRGBO(255, 255, 255, 1).withValues(alpha: 0.5); Color get dotsIndicatorInactiveColor => brightness == Brightness.light - ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.12) - : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.12); + ? const Color.fromRGBO(0, 0, 0, 1).withValues(alpha: 0.12) + : const Color.fromRGBO(255, 255, 255, 1).withValues(alpha: 0.12); Color get toastTextColor => brightness == Brightness.light ? const Color.fromRGBO(255, 255, 255, 1) @@ -336,8 +340,8 @@ extension CustomColorScheme on ColorScheme { : const Color.fromRGBO(150, 150, 150, 1); Color get searchResultsBackgroundColor => brightness == Brightness.light - ? Colors.black.withOpacity(0.32) - : Colors.black.withOpacity(0.64); + ? Colors.black.withValues(alpha: 0.32) + : Colors.black.withValues(alpha: 0.64); EnteTheme get enteTheme => brightness == Brightness.light ? lightTheme : darkTheme; diff --git a/mobile/lib/events/album_sort_order_change_event.dart b/mobile/lib/events/album_sort_order_change_event.dart new file mode 100644 index 0000000000..29c40477f1 --- /dev/null +++ b/mobile/lib/events/album_sort_order_change_event.dart @@ -0,0 +1,3 @@ +import "package:photos/events/event.dart"; + +class AlbumSortOrderChangeEvent extends Event {} diff --git a/mobile/lib/events/clear_album_selections_event.dart b/mobile/lib/events/clear_album_selections_event.dart new file mode 100644 index 0000000000..1d7ddb7011 --- /dev/null +++ b/mobile/lib/events/clear_album_selections_event.dart @@ -0,0 +1,3 @@ +import "package:photos/events/event.dart"; + +class ClearAlbumSelectionsEvent extends Event {} diff --git a/mobile/lib/events/preview_updated_event.dart b/mobile/lib/events/preview_updated_event.dart deleted file mode 100644 index 8e9cb65aaa..0000000000 --- a/mobile/lib/events/preview_updated_event.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "dart:collection"; - -import "package:photos/events/event.dart"; -import "package:photos/models/preview/preview_item.dart"; - -class PreviewUpdatedEvent extends Event { - final LinkedHashMap items; - - PreviewUpdatedEvent(this.items); -} diff --git a/mobile/lib/extensions/user_extension.dart b/mobile/lib/extensions/user_extension.dart index 20a36030b3..5204847724 100644 --- a/mobile/lib/extensions/user_extension.dart +++ b/mobile/lib/extensions/user_extension.dart @@ -12,4 +12,12 @@ extension UserExtension on User { String? get linkedPersonID => PersonService.instance.emailToPartialPersonDataMapCache[email] ?[PersonService.kPersonIDKey]; + + String get nameOrEmail { + if (PersonService.isInitialized) { + return displayName ?? email.substring(0, email.indexOf("@")); + } else { + return email.substring(0, email.indexOf("@")); + } + } } diff --git a/mobile/lib/generated/intl/messages_all.dart b/mobile/lib/generated/intl/messages_all.dart index 148e355746..075ef6fbf8 100644 --- a/mobile/lib/generated/intl/messages_all.dart +++ b/mobile/lib/generated/intl/messages_all.dart @@ -27,6 +27,7 @@ import 'messages_el.dart' as messages_el; import 'messages_en.dart' as messages_en; import 'messages_es.dart' as messages_es; import 'messages_et.dart' as messages_et; +import 'messages_eu.dart' as messages_eu; import 'messages_fa.dart' as messages_fa; import 'messages_fr.dart' as messages_fr; import 'messages_gu.dart' as messages_gu; @@ -51,6 +52,7 @@ import 'messages_pt_PT.dart' as messages_pt_pt; import 'messages_ro.dart' as messages_ro; import 'messages_ru.dart' as messages_ru; import 'messages_sl.dart' as messages_sl; +import 'messages_sr.dart' as messages_sr; import 'messages_sv.dart' as messages_sv; import 'messages_ta.dart' as messages_ta; import 'messages_te.dart' as messages_te; @@ -74,6 +76,7 @@ Map _deferredLibraries = { 'en': () => new SynchronousFuture(null), 'es': () => new SynchronousFuture(null), 'et': () => new SynchronousFuture(null), + 'eu': () => new SynchronousFuture(null), 'fa': () => new SynchronousFuture(null), 'fr': () => new SynchronousFuture(null), 'gu': () => new SynchronousFuture(null), @@ -98,6 +101,7 @@ Map _deferredLibraries = { 'ro': () => new SynchronousFuture(null), 'ru': () => new SynchronousFuture(null), 'sl': () => new SynchronousFuture(null), + 'sr': () => new SynchronousFuture(null), 'sv': () => new SynchronousFuture(null), 'ta': () => new SynchronousFuture(null), 'te': () => new SynchronousFuture(null), @@ -133,6 +137,8 @@ MessageLookupByLibrary? _findExact(String localeName) { return messages_es.messages; case 'et': return messages_et.messages; + case 'eu': + return messages_eu.messages; case 'fa': return messages_fa.messages; case 'fr': @@ -181,6 +187,8 @@ MessageLookupByLibrary? _findExact(String localeName) { return messages_ru.messages; case 'sl': return messages_sl.messages; + case 'sr': + return messages_sr.messages; case 'sv': return messages_sv.messages; case 'ta': diff --git a/mobile/lib/generated/intl/messages_ar.dart b/mobile/lib/generated/intl/messages_ar.dart index 81d1cf79c2..fa1bd688e2 100644 --- a/mobile/lib/generated/intl/messages_ar.dart +++ b/mobile/lib/generated/intl/messages_ar.dart @@ -41,7 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(name) => "الإعجاب بـ ${name}"; static String m8(count) => - "${Intl.plural(count, zero: 'لا يوجد مشاركون', one: 'مشارك واحد', two: 'مشاركان', few: '${count} مشاركين', many: '${count} مشاركًا', other: '${count} مشارك')}"; + "${Intl.plural(count, zero: 'لا يوجد مُشاركون', one: 'مُشارك واحد', other: '${count} مُشاركين')}"; static String m9(versionValue) => "الإصدار: ${versionValue}"; @@ -89,7 +89,7 @@ class MessageLookup extends MessageLookupByLibrary { "سيؤدي هذا إلى إزالة الرابط العام للوصول إلى \"${albumName}\"."; static String m24(supportEmail) => - "يرجى إرسال بريد إلكتروني إلى ${supportEmail} من عنوان بريدك الإلكتروني المسجل."; + "يرجى إرسال بريد إلكتروني إلى ${supportEmail} من عنوان بريدك الإلكتروني المسجل"; static String m25(count, storageSaved) => "لقد قمت بتنظيف ${Intl.plural(count, one: 'ملف مكرر واحد', two: 'ملفين مكررين', few: '${count} ملفات مكررة', many: '${count} ملفًا مكررًا', other: '${count} ملفًا مكررًا')}، مما وفر ${storageSaved}!"; @@ -97,217 +97,224 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} ملفات، ${formattedSize} لكل منها"; - static String m27(newEmail) => "تم تغيير البريد الإلكتروني إلى ${newEmail}"; + static String m27(name) => "هذا البريد الإلكتروني مرتبط مسبقاً بـ ${name}."; - static String m28(email) => "${email} لا يملك حساب Ente."; + static String m28(newEmail) => "تم تغيير البريد الإلكتروني إلى ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} لا يملك حساب Ente."; + + static String m30(email) => "${email} لا يملك حسابًا على Ente.\n\nأرسل له دعوة لمشاركة الصور."; - static String m30(name) => "معانقة ${name}"; + static String m31(name) => "معانقة ${name}"; - static String m31(text) => "تم العثور على صور إضافية لـ ${text}"; + static String m32(text) => "تم العثور على صور إضافية لـ ${text}"; - static String m32(name) => "الاستمتاع بالطعام مع ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} على هذا الجهاز تم نسخه احتياطيًا بأمان"; + static String m33(name) => "الاستمتاع بالطعام مع ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} على هذا الجهاز تم نسخه احتياطيًا بأمان"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: 'ملف واحد', two: 'ملفان', few: '${formattedNumber} ملفات', many: '${formattedNumber} ملفًا', other: '${formattedNumber} ملفًا')} في هذا الألبوم تم نسخه احتياطيًا بأمان"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} جيجابايت مجانية في كل مرة يشترك فيها شخص بخطة مدفوعة ويطبق رمزك"; - static String m36(endDate) => "التجربة المجانية صالحة حتى ${endDate}"; + static String m37(endDate) => "التجربة المجانية صالحة حتى ${endDate}"; - static String m37(count) => + static String m38(count) => "لا يزال بإمكانك الوصول ${Intl.plural(count, one: 'إليه', two: 'إليهما', other: 'إليها')} على Ente طالما لديك اشتراك نشط."; - static String m38(sizeInMBorGB) => "تحرير ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "تحرير ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'يمكن حذفه من الجهاز لتحرير ${formattedSize}', two: 'يمكن حذفهما من الجهاز لتحرير ${formattedSize}', few: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}', many: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}', other: 'يمكن حذفها من الجهاز لتحرير ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "جارٍ المعالجة ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "التنزه مع ${name}"; + static String m42(name) => "التنزه مع ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} عُنْصُر', other: '${count} عَنَاصِر')}"; - static String m43(name) => "آخر مرة مع ${name}"; + static String m44(name) => "آخر مرة مع ${name}"; - static String m44(email) => "${email} دعاك لتكون جهة اتصال موثوقة"; + static String m45(email) => "${email} دعاك لتكون جهة اتصال موثوقة"; - static String m45(expiryTime) => "ستنتهي صلاحية الرابط في ${expiryTime}"; + static String m46(expiryTime) => "ستنتهي صلاحية الرابط في ${expiryTime}"; - static String m46(email) => "ربط الشخص بـ ${email}"; + static String m47(email) => "ربط الشخص بـ ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "سيؤدي هذا إلى ربط ${personName} بـ ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'لا توجد ذكريات', one: 'ذكرى واحدة', two: 'ذكريتان', few: '${formattedCount} ذكريات', many: '${formattedCount} ذكرى', other: '${formattedCount} ذكرى')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'نقل عنصر', two: 'نقل عنصرين', few: 'نقل ${count} عناصر', many: 'نقل ${count} عنصرًا', other: 'نقل ${count} عنصرًا')}"; - static String m50(albumName) => "تم النقل بنجاح إلى ${albumName}"; + static String m51(albumName) => "تم النقل بنجاح إلى ${albumName}"; - static String m51(personName) => "لا توجد اقتراحات لـ ${personName}"; + static String m52(personName) => "لا توجد اقتراحات لـ ${personName}"; - static String m52(name) => "ليس ${name}؟"; + static String m53(name) => "ليس ${name}؟"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "يرجى الاتصال بـ ${familyAdminEmail} لتغيير الرمز الخاص بك."; - static String m54(name) => "الاحتفال مع ${name}"; + static String m55(name) => "الاحتفال مع ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "قوة كلمة المرور: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "يرجى التواصل مع دعم ${providerName} إذا تم خصم المبلغ منك."; - static String m57(name, age) => "${name} يبلغ ${age}!"; + static String m58(name, age) => "${name} يبلغ ${age}!"; - static String m58(name, age) => "${name} سيبلغ ${age} قريبًا"; - - static String m59(count) => - "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; + static String m59(name, age) => "${name} سيبلغ ${age} قريبًا"; static String m60(count) => "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: 'لا توجد صور', one: 'صورة واحدة', two: 'صورتان', few: '${count} صور', many: '${count} صورة', other: '${count} صورة')}"; + + static String m62(endDate) => "التجربة المجانية صالحة حتى ${endDate}.\nيمكنك اختيار خطة مدفوعة بعد ذلك."; - static String m62(toEmail) => + static String m63(toEmail) => "يرجى مراسلتنا عبر البريد الإلكتروني على ${toEmail}"; - static String m63(toEmail) => "يرجى إرسال السجلات إلى \n${toEmail}"; + static String m64(toEmail) => "يرجى إرسال السجلات إلى \n${toEmail}"; - static String m64(name) => "التقاط صور مع ${name}"; + static String m65(name) => "التقاط صور مع ${name}"; - static String m65(folderName) => "جارٍ معالجة ${folderName}..."; + static String m66(folderName) => "جارٍ معالجة ${folderName}..."; - static String m66(storeName) => "قيّمنا على ${storeName}"; + static String m67(storeName) => "قيّمنا على ${storeName}"; - static String m67(name) => "تمت إعادة تعيينك إلى ${name}"; + static String m68(name) => "تمت إعادة تعيينك إلى ${name}"; - static String m68(days, email) => + static String m69(days, email) => "يمكنك الوصول إلى الحساب بعد ${days} أيام. سيتم إرسال إشعار إلى ${email}."; - static String m69(email) => + static String m70(email) => "يمكنك الآن استرداد حساب ${email} عن طريق تعيين كلمة مرور جديدة."; - static String m70(email) => "${email} يحاول استرداد حسابك."; + static String m71(email) => "${email} يحاول استرداد حسابك."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. تحصلون كلاكما على ${storageInGB} جيجابايت* مجانًا"; - static String m72(userEmail) => + static String m73(userEmail) => "سيتم إزالة ${userEmail} من هذا الألبوم المشترك.\n\nسيتم أيضًا إزالة أي صور أضافها إلى الألبوم."; - static String m73(endDate) => "يتجدد الاشتراك في ${endDate}"; + static String m74(endDate) => "يتجدد الاشتراك في ${endDate}"; - static String m74(name) => "رحلة برية مع ${name}"; + static String m75(name) => "رحلة برية مع ${name}"; - static String m75(snapshotLength, searchLength) => + static String m76(count) => + "${Intl.plural(count, one: '${count} النتائج التي تم العثور عليها', other: '${count} النتائج التي تم العثور عليها')}"; + + static String m77(snapshotLength, searchLength) => "عدم تطابق طول الأقسام: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "تم تحديد ${count}"; + static String m78(count) => "تم تحديد ${count}"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "تم تحديد ${count} (${yourCount} منها لك)"; - static String m78(name) => "صور سيلفي مع ${name}"; + static String m80(name) => "صور سيلفي مع ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "إليك معرّف التحقق الخاص بي لـ ente.io: ${verificationID}"; - static String m80(verificationID) => - "مرحبًا، هل يمكنك تأكيد أن هذا هو معرّف التحقق الخاص بك على ente.io: ${verificationID}؟"; + static String m82(verificationID) => + "مرحبًا، هل يمكنك تأكيد أن هذا هو معرّف التحقق الخاص بك على ente.io: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "رمز إحالة Ente الخاص بي: ${referralCode}\n\nطبقه في الإعدادات ← عام ← الإحالات للحصول على ${referralStorageInGB} جيجابايت مجانًا بعد الاشتراك في خطة مدفوعة.\n\nhttps://ente.io"; - static String m82(numberOfPeople) => - "${Intl.plural(numberOfPeople, zero: 'مشاركة مع أشخاص محددين', one: 'تمت المشاركة مع شخص واحد', two: 'تمت المشاركة مع شخصين', few: 'تمت المشاركة مع ${numberOfPeople} أشخاص', many: 'تمت المشاركة مع ${numberOfPeople} شخصًا', other: 'تمت المشاركة مع ${numberOfPeople} شخصًا')}"; + static String m84(numberOfPeople) => + "${Intl.plural(numberOfPeople, zero: 'مشاركة مع أشخاص مُحددين', one: 'مُشارَك مع شخص واحد', other: 'مُشارَك مع ${numberOfPeople} أشخاص')}"; - static String m83(emailIDs) => "تمت المشاركة مع ${emailIDs}"; + static String m85(emailIDs) => "تمت المشاركة مع ${emailIDs}"; - static String m84(fileType) => "سيتم حذف ${fileType} من جهازك."; + static String m86(fileType) => "سيتم حذف ${fileType} من جهازك."; - static String m85(fileType) => "${fileType} موجود في Ente وعلى جهازك."; + static String m87(fileType) => "${fileType} موجود في Ente وعلى جهازك."; - static String m86(fileType) => "سيتم حذف ${fileType} من Ente."; + static String m88(fileType) => "سيتم حذف ${fileType} من Ente."; - static String m87(name) => "الرياضة مع ${name}"; + static String m89(name) => "الرياضة مع ${name}"; - static String m88(name) => "تسليط الضوء على ${name}"; + static String m90(name) => "تسليط الضوء على ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} جيجابايت"; + static String m91(storageAmountInGB) => "${storageAmountInGB} جيجابايت"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "تم استخدام ${usedAmount} ${usedStorageUnit} من ${totalAmount} ${totalStorageUnit}"; - static String m91(id) => + static String m93(id) => "تم ربط ${id} الخاص بك بحساب Ente آخر.\nإذا كنت ترغب في استخدام ${id} مع هذا الحساب، يرجى الاتصال بدعمنا."; - static String m92(endDate) => "سيتم إلغاء اشتراكك في ${endDate}"; + static String m94(endDate) => "سيتم إلغاء اشتراكك في ${endDate}"; - static String m93(completed, total) => "${completed}/${total} ذكريات محفوظة"; + static String m95(completed, total) => "${completed}/${total} ذكريات محفوظة"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "انقر للتحميل، تم تجاهل التحميل حاليًا بسبب ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "سيحصلون أيضًا على ${storageAmountInGB} جيجابايت"; - static String m96(email) => "هذا هو معرّف التحقق الخاص بـ ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'هذا الأسبوع، قبل سنة', two: 'هذا الأسبوع، قبل سنتين', few: 'هذا الأسبوع، قبل ${count} سنوات', many: 'هذا الأسبوع، قبل ${count} سنة', other: 'هذا الأسبوع، قبل ${count} سنة')}"; - - static String m98(dateFormat) => "${dateFormat} عبر السنين"; + static String m98(email) => "هذا هو معرّف التحقق الخاص بـ ${email}"; static String m99(count) => + "${Intl.plural(count, one: 'هذا الأسبوع، قبل سنة', two: 'هذا الأسبوع، قبل سنتين', few: 'هذا الأسبوع، قبل ${count} سنوات', many: 'هذا الأسبوع، قبل ${count} سنة', other: 'هذا الأسبوع، قبل ${count} سنة')}"; + + static String m100(dateFormat) => "${dateFormat} عبر السنين"; + + static String m101(count) => "${Intl.plural(count, zero: 'قريبًا', one: 'يوم واحد', two: 'يومان', few: '${count} أيام', many: '${count} يومًا', other: '${count} يومًا')}"; - static String m100(year) => "رحلة في ${year}"; + static String m102(year) => "رحلة في ${year}"; - static String m101(location) => "رحلة إلى ${location}"; + static String m103(location) => "رحلة إلى ${location}"; - static String m102(email) => + static String m104(email) => "لقد تمت دعوتك لتكون جهة اتصال موثوقة بواسطة ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "نوع المعرض ${galleryType} غير مدعوم لإعادة التسمية."; - static String m104(ignoreReason) => "تم تجاهل التحميل بسبب ${ignoreReason}"; + static String m106(ignoreReason) => "تم تجاهل التحميل بسبب ${ignoreReason}"; - static String m105(count) => "جارٍ حفظ ${count} ذكريات..."; + static String m107(count) => "جارٍ حفظ ${count} ذكريات..."; - static String m106(endDate) => "صالح حتى ${endDate}"; + static String m108(endDate) => "صالح حتى ${endDate}"; - static String m107(email) => "التحقق من ${email}"; + static String m109(email) => "التحقق من ${email}"; - static String m108(count) => + static String m110(name) => "عرض ${name} لإلغاء الربط"; + + static String m111(count) => "${Intl.plural(count, zero: 'تمت إضافة 0 مشاهدين', one: 'تمت إضافة مشاهد واحد', two: 'تمت إضافة مشاهدين', few: 'تمت إضافة ${count} مشاهدين', many: 'تمت إضافة ${count} مشاهدًا', other: 'تمت إضافة ${count} مشاهدًا')}"; - static String m109(email) => + static String m112(email) => "لقد أرسلنا بريدًا إلكترونيًا إلى ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'قبل سنة', two: 'قبل سنتين', few: 'قبل ${count} سنوات', many: 'قبل ${count} سنة', other: 'قبل ${count} سنة')}"; - static String m111(name) => "أنت و ${name}"; + static String m114(name) => "أنت و ${name}"; - static String m112(storageSaved) => "لقد حررت ${storageSaved} بنجاح!"; + static String m115(storageSaved) => "لقد حررت ${storageSaved} بنجاح!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -321,7 +328,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("الحساب تم تكوينه بالفعل."), "accountOwnerPersonAppbarTitle": m0, "accountWelcomeBack": - MessageLookupByLibrary.simpleMessage("مرحبًا مجددًا!"), + MessageLookupByLibrary.simpleMessage("أهلاً بعودتك!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "أدرك أنني إذا فقدت كلمة المرور، فقد أفقد بياناتي لأنها مشفرة بالكامل من طرف إلى طرف."), "activeSessions": @@ -452,36 +459,36 @@ class MessageLookup extends MessageLookupByLibrary { "هل أنت متأكد من رغبتك في إعادة تعيين هذا الشخص؟"), "askCancelReason": MessageLookupByLibrary.simpleMessage( "تم إلغاء اشتراكك. هل ترغب في مشاركة السبب؟"), - "askDeleteReason": MessageLookupByLibrary.simpleMessage( - "ما السبب الرئيسي لحذف حسابك؟"), + "askDeleteReason": + MessageLookupByLibrary.simpleMessage("ما السبب الرئيس لحذف حسابك؟"), "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage("اطلب من أحبائك المشاركة"), "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("في ملجأ للطوارئ"), "authToChangeEmailVerificationSetting": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة لتغيير إعداد التحقق من البريد الإلكتروني."), + "يرجى المصادقة لتغيير إعداد التحقق من البريد الإلكتروني"), "authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage( "يرجى المصادقة لتغيير إعدادات شاشة القفل."), "authToChangeYourEmail": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة لتغيير بريدك الإلكتروني."), + "يرجى المصادقة لتغيير بريدك الإلكتروني"), "authToChangeYourPassword": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة لتغيير كلمة المرور الخاصة بك."), + "يرجى المصادقة لتغيير كلمة المرور الخاصة بك"), "authToConfigureTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( "يرجى المصادقة لإعداد المصادقة الثنائية."), "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة لبدء عملية حذف الحساب."), + "يرجى المصادقة لبدء عملية حذف الحساب"), "authToManageLegacy": MessageLookupByLibrary.simpleMessage( "يرجى المصادقة لإدارة جهات الاتصال الموثوقة الخاصة بك."), "authToViewPasskey": MessageLookupByLibrary.simpleMessage( "يرجى المصادقة لعرض مفتاح المرور الخاص بك."), "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة لعرض ملفاتك المحذوفة."), + "يرجى المصادقة لعرض ملفاتك المحذوفة"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( "يرجى المصادقة لعرض جلساتك النشطة."), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( - "يرجى المصادقة للوصول إلى ملفاتك المخفية."), + "يرجى المصادقة للوصول إلى ملفاتك المخفية"), "authToViewYourMemories": MessageLookupByLibrary.simpleMessage("يرجى المصادقة لعرض ذكرياتك."), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -526,27 +533,10 @@ class MessageLookup extends MessageLookupByLibrary { "النسخ الاحتياطي لمقاطع الفيديو"), "beach": MessageLookupByLibrary.simpleMessage("رمال وبحر"), "birthday": MessageLookupByLibrary.simpleMessage("تاريخ الميلاد"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("تخفيضات الجمعة السوداء"), "blog": MessageLookupByLibrary.simpleMessage("المدونة"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("تعديل التواريخ بشكل جماعي"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "يمكنك الآن تحديد صور متعددة، وتعديل التاريخ/الوقت لجميعها بإجراء سريع واحد. تغيير التواريخ مدعوم أيضًا."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("حدود الخطة العائلية"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "يمكنك الآن تعيين حدود لمقدار التخزين الذي يمكن لأفراد عائلتك استخدامه."), - "cLIcon": MessageLookupByLibrary.simpleMessage("أيقونة جديدة"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "أخيرًا، أيقونة تطبيق جديدة، نعتقد أنها تمثل عملنا على أفضل وجه. أضفنا أيضًا مبدل أيقونات حتى تتمكن من الاستمرار في استخدام الأيقونة القديمة."), - "cLMemories": MessageLookupByLibrary.simpleMessage("الذكريات"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "أعد اكتشاف لحظاتك الخاصة - تسليط الضوء على الأشخاص المفضلين لديك، رحلاتك وعطلاتك، أفضل لقطاتك، وأكثر من ذلك بكثير. قم بتشغيل تعلم الآلة، ضع علامة على نفسك وقم بتسمية أصدقائك للحصول على أفضل تجربة."), - "cLWidgets": - MessageLookupByLibrary.simpleMessage("الأدوات المصغرة (Widgets)"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "الأدوات المصغرة للشاشة الرئيسية المدمجة مع الذكريات متاحة الآن. ستعرض لحظاتك الخاصة دون فتح التطبيق."), "cachedData": MessageLookupByLibrary.simpleMessage("البيانات المؤقتة"), "calculating": MessageLookupByLibrary.simpleMessage("جارٍ الحساب..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -571,7 +561,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("إلغاء الاشتراك"), "cannotAddMorePhotosAfterBecomingViewer": m13, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( - "لا يمكن حذف الملفات المشتركة."), + "لا يمكن حذف الملفات المشتركة"), "castAlbum": MessageLookupByLibrary.simpleMessage("بث الألبوم"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( "يرجى التأكد من أنك متصل بنفس الشبكة المتصل بها التلفزيون."), @@ -617,6 +607,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• انقر على"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• انقر على قائمة الخيارات الإضافية"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("إغلاق"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("التجميع حسب وقت الالتقاط"), @@ -633,7 +625,7 @@ class MessageLookup extends MessageLookupByLibrary { "codeUsedByYou": MessageLookupByLibrary.simpleMessage("الرمز المستخدم من قبلك"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( - "أنشئ رابطًا يسمح للأشخاص بإضافة الصور ومشاهدتها في ألبومك المشترك دون الحاجة إلى تطبيق Ente أو حساب. خيار مثالي لجمع صور الفعاليات بسهولة."), + "أنشئ رابطًا يسمح للأشخاص بإضافة الصور ومشاهدتها في ألبومك المشترك دون الحاجة إلى تطبيق أو حساب Ente. خيار مثالي لجمع صور الفعاليات بسهولة."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("رابط تعاوني"), "collaborativeLinkCreatedFor": m15, @@ -717,7 +709,7 @@ class MessageLookup extends MessageLookupByLibrary { "crop": MessageLookupByLibrary.simpleMessage("اقتصاص"), "curatedMemories": MessageLookupByLibrary.simpleMessage("ذكريات منسقة"), "currentUsageIs": - MessageLookupByLibrary.simpleMessage("استخدامك الحالي هو"), + MessageLookupByLibrary.simpleMessage("استخدامك الحالي هو "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("قيد التشغيل حاليًا"), "custom": MessageLookupByLibrary.simpleMessage("مخصص"), @@ -757,15 +749,15 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("الحذف من كليهما"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("الحذف من الجهاز"), - "deleteFromEnte": MessageLookupByLibrary.simpleMessage("الحذف من Ente"), + "deleteFromEnte": MessageLookupByLibrary.simpleMessage("حذف من Ente"), "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("حذف الموقع"), "deletePhotos": MessageLookupByLibrary.simpleMessage("حذف الصور"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( - "تفتقد إلى ميزة أساسية أحتاجها"), + "تفتقر إلى مِيزة أساسية أحتاج إليها"), "deleteReason2": MessageLookupByLibrary.simpleMessage( - "التطبيق أو ميزة معينة لا تعمل كما هو متوقع"), + "التطبيق أو مِيزة معينة لا تعمل كما هو متوقع"), "deleteReason3": MessageLookupByLibrary.simpleMessage("وجدت خدمة أخرى أفضل"), "deleteReason4": MessageLookupByLibrary.simpleMessage("سببي غير مدرج"), @@ -795,7 +787,7 @@ class MessageLookup extends MessageLookupByLibrary { "disableAutoLock": MessageLookupByLibrary.simpleMessage("تعطيل القفل التلقائي"), "disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage( - "لا يزال بإمكان المشاهدين التقاط لقطات شاشة أو حفظ نسخة من صورك باستخدام أدوات خارجية."), + "لا يزال بإمكان المشاهدين التقاط لقطات شاشة أو حفظ نسخة من صورك باستخدام أدوات خارجية"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("يرجى الملاحظة"), "disableLinkMessage": m23, @@ -845,6 +837,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("تعديل"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("تعديل الموقع"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("تعديل الموقع"), @@ -858,16 +851,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("البريد الإلكتروني"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "البريد الإلكتروني مُسجل من قبل."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("البريد الإلكتروني غير مسجل."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "تأكيد عنوان البريد الإلكتروني"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "إرسال سجلاتك عبر البريد الإلكتروني"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("جهات اتصال الطوارئ"), "empty": MessageLookupByLibrary.simpleMessage("إفراغ"), @@ -893,7 +886,7 @@ class MessageLookup extends MessageLookupByLibrary { "تشفير من طرف إلى طرف بشكل افتراضي"), "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": MessageLookupByLibrary.simpleMessage( - "يمكن لـ Ente تشفير وحفظ الملفات فقط إذا منحت الإذن بالوصول إليها."), + "يمكن لـ Ente تشفير وحفظ الملفات فقط إذا منحت الإذن بالوصول إليها"), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "Ente بحاجة إلى إذن لحفظ صورك"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( @@ -904,7 +897,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("أدخل اسم الألبوم"), "enterCode": MessageLookupByLibrary.simpleMessage("أدخل الرمز"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( - "أدخل الرمز المقدم من صديقك للمطالبة بمساحة تخزين مجانية لكما."), + "أدخل الرمز المقدم من صديقك للمطالبة بمساحة تخزين مجانية لكما"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("تاريخ الميلاد (اختياري)"), "enterEmail": @@ -929,6 +922,8 @@ class MessageLookup extends MessageLookupByLibrary { "يرجى إدخال عنوان بريد إلكتروني صالح."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("أدخل عنوان بريدك الإلكتروني"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "أدخل عنوان بريدك الإلكتروني الجديد"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("أدخل كلمة المرور"), "enterYourRecoveryKey": @@ -943,7 +938,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("تصدير بياناتك"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("تم العثور على صور إضافية"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "لم يتم تجميع الوجه بعد، يرجى العودة لاحقًا"), "faceRecognition": @@ -978,7 +973,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("الأسئلة الشائعة"), "faqs": MessageLookupByLibrary.simpleMessage("الأسئلة الشائعة"), "favorite": MessageLookupByLibrary.simpleMessage("المفضلة"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("ملاحظات"), "file": MessageLookupByLibrary.simpleMessage("ملف"), "fileFailedToSaveToGallery": @@ -992,8 +987,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("أنواع الملفات"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("أنواع وأسماء الملفات"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("تم حذف الملفات."), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("تم حفظ الملفات في المعرض."), @@ -1005,37 +1000,37 @@ class MessageLookup extends MessageLookupByLibrary { "food": MessageLookupByLibrary.simpleMessage("متعة الطهي"), "forYourMemories": MessageLookupByLibrary.simpleMessage("لذكرياتك"), "forgotPassword": - MessageLookupByLibrary.simpleMessage("نسيت كلمة المرور؟"), + MessageLookupByLibrary.simpleMessage("نسيت كلمة المرور"), "foundFaces": MessageLookupByLibrary.simpleMessage("الوجوه التي تم العثور عليها"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "تم المطالبة بمساحة التخزين المجانية"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "مساحة تخزين مجانية متاحة للاستخدام"), "freeTrial": MessageLookupByLibrary.simpleMessage("تجربة مجانية"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("تحرير مساحة على الجهاز"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "وفر مساحة على جهازك عن طريق مسح الملفات التي تم نسخها احتياطيًا."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("تحرير المساحة"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("المعرض"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "يتم عرض ما يصل إلى 1000 ذكرى في المعرض."), "general": MessageLookupByLibrary.simpleMessage("عام"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "جارٍ إنشاء مفاتيح التشفير..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("الانتقال إلى الإعدادات"), "googlePlayId": MessageLookupByLibrary.simpleMessage("معرّف Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( - "يرجى السماح بالوصول إلى جميع الصور في تطبيق الإعدادات."), + "الرجاء السماح بالوصول إلى جميع الصور في تطبيق الإعدادات"), "grantPermission": MessageLookupByLibrary.simpleMessage("منح الإذن"), "greenery": MessageLookupByLibrary.simpleMessage("الحياة الخضراء"), "groupNearbyPhotos": @@ -1043,6 +1038,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("عرض الضيف"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "لتمكين عرض الضيف، يرجى إعداد رمز مرور الجهاز أو قفل الشاشة في إعدادات النظام."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "نحن لا نتتبع عمليات تثبيت التطبيق. سيساعدنا إذا أخبرتنا أين وجدتنا!"), "hearUsWhereTitle": @@ -1058,7 +1055,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "إخفاء العناصر المشتركة من معرض الصفحة الرئيسية"), "hiding": MessageLookupByLibrary.simpleMessage("جارٍ الإخفاء..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("مستضاف في OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("كيف يعمل"), @@ -1113,7 +1110,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "يبدو أن خطأً ما قد حدث. يرجى المحاولة مرة أخرى بعد بعض الوقت. إذا استمر الخطأ، يرجى الاتصال بفريق الدعم لدينا."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "تعرض العناصر عدد الأيام المتبقية قبل الحذف الدائم."), @@ -1135,7 +1132,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "يرجى مساعدتنا بهذه المعلومات"), "language": MessageLookupByLibrary.simpleMessage("اللغة"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("آخر تحديث"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("رحلة العام الماضي"), @@ -1149,7 +1146,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("جهات الاتصال الموثوقة"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("الحسابات الموثوقة"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "تسمح جهات الاتصال الموثوقة لأشخاص معينين بالوصول إلى حسابك في حالة غيابك."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1166,7 +1163,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لمشاركة أسرع"), "linkEnabled": MessageLookupByLibrary.simpleMessage("مفعّل"), "linkExpired": MessageLookupByLibrary.simpleMessage("منتهي الصلاحية"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("انتهاء صلاحية الرابط"), "linkHasExpired": @@ -1175,13 +1172,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("ربط الشخص"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("لتجربة مشاركة أفضل"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("الصور الحية"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "يمكنك مشاركة اشتراكك مع عائلتك."), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "لقد حفظنا أكثر من 30 مليون ذكرى حتى الآن."), + "لقد حفظنا أكثر من 200 مليون ذكرى حتى الآن"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "نحتفظ بـ 3 نسخ من بياناتك، إحداها في ملجأ للطوارئ تحت الأرض."), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1238,6 +1235,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "اضغط مطولاً على عنصر لعرضه في وضع ملء الشاشة."), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("إيقاف تكرار الفيديو"), "loopVideoOn": @@ -1265,7 +1264,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("أنا"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("المنتجات الترويجية"), "mergeWithExisting": @@ -1297,13 +1296,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("الأحدث"), "mostRelevant": MessageLookupByLibrary.simpleMessage("الأكثر صلة"), "mountains": MessageLookupByLibrary.simpleMessage("فوق التلال"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "نقل الصور المحددة إلى تاريخ واحد"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("نقل إلى ألبوم"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("نقل إلى الألبوم المخفي"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("تم النقل إلى سلة المهملات"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1318,6 +1317,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("ألبوم جديد"), "newLocation": MessageLookupByLibrary.simpleMessage("موقع جديد"), "newPerson": MessageLookupByLibrary.simpleMessage("شخص جديد"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("نطاق جديد"), "newToEnte": MessageLookupByLibrary.simpleMessage("جديد في Ente"), "newest": MessageLookupByLibrary.simpleMessage("الأحدث"), @@ -1329,7 +1329,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لم يتم العثور على جهاز."), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("لا شيء"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "لا توجد ملفات على هذا الجهاز يمكن حذفها."), + "لا توجد ملفات على هذا الجهاز يمكن حذفها"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ لا توجد ملفات مكررة"), "noEnteAccountExclamation": @@ -1346,7 +1346,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لا يوجد اتصال بالإنترنت"), "noPhotosAreBeingBackedUpRightNow": MessageLookupByLibrary.simpleMessage( - "لا يتم نسخ أي صور احتياطيًا في الوقت الحالي."), + "لا يتم نسخ أي صور احتياطيًا في الوقت الحالي"), "noPhotosFoundHere": MessageLookupByLibrary.simpleMessage("لم يتم العثور على صور هنا"), "noQuickLinksSelected": @@ -1354,14 +1354,14 @@ class MessageLookup extends MessageLookupByLibrary { "noRecoveryKey": MessageLookupByLibrary.simpleMessage("لا تملك مفتاح استرداد؟"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( - "نظرًا لطبيعة التشفير الكامل من طرف إلى طرف، لا يمكن فك تشفير بياناتك بدون كلمة المرور أو مفتاح الاسترداد الخاص بك."), + "نظرًا لطبيعة التشفير الكامل من طرف إلى طرف، لا يمكن فك تشفير بياناتك دون كلمة المرور أو مفتاح الاسترداد الخاص بك"), "noResults": MessageLookupByLibrary.simpleMessage("لا توجد نتائج"), "noResultsFound": MessageLookupByLibrary.simpleMessage("لم يتم العثور على نتائج."), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("لم يتم العثور على قفل نظام."), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("ليس هذا الشخص؟"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "لم تتم مشاركة أي شيء معك بعد"), @@ -1374,7 +1374,10 @@ class MessageLookup extends MessageLookupByLibrary { "على Ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("على الطريق مرة أخرى"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("هم فقط"), "oops": MessageLookupByLibrary.simpleMessage("عفوًا"), "oopsCouldNotSaveEdits": @@ -1404,7 +1407,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("اكتمل الإقران"), "panorama": MessageLookupByLibrary.simpleMessage("بانوراما"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("التحقق لا يزال معلقًا."), "passkey": MessageLookupByLibrary.simpleMessage("مفتاح المرور"), @@ -1412,19 +1415,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("التحقق من مفتاح المرور"), "password": MessageLookupByLibrary.simpleMessage("كلمة المرور"), "passwordChangedSuccessfully": - MessageLookupByLibrary.simpleMessage("تم تغيير كلمة المرور بنجاح."), + MessageLookupByLibrary.simpleMessage("تم تغيير كلمة المرور بنجاح"), "passwordLock": MessageLookupByLibrary.simpleMessage("قفل بكلمة مرور"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "يتم حساب قوة كلمة المرور مع الأخذ في الاعتبار طول كلمة المرور، والأحرف المستخدمة، وما إذا كانت كلمة المرور تظهر في قائمة أفضل 10,000 كلمة مرور شائعة الاستخدام."), "passwordWarning": MessageLookupByLibrary.simpleMessage( - "نحن لا نخزن كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا المساعدة في فك تشفير بياناتك."), + "نحن لا نقوم بتخزين كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا فك تشفير بياناتك"), "paymentDetails": MessageLookupByLibrary.simpleMessage("تفاصيل الدفع"), "paymentFailed": MessageLookupByLibrary.simpleMessage("فشلت عملية الدفع"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "للأسف، فشلت عملية الدفع الخاصة بك. يرجى الاتصال بالدعم وسوف نساعدك!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("العناصر المعلقة"), "pendingSync": MessageLookupByLibrary.simpleMessage("المزامنة المعلقة"), "people": MessageLookupByLibrary.simpleMessage("الأشخاص"), @@ -1435,20 +1438,20 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("حذف نهائي"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("حذف نهائي من الجهاز؟"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("اسم الشخص"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("رفاق فروي"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("أوصاف الصور"), "photoGridSize": MessageLookupByLibrary.simpleMessage("حجم شبكة الصور"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("صورة"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("الصور"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "ستتم إزالة الصور التي أضفتها من الألبوم."), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "تحتفظ الصور بالفرق الزمني النسبي"), @@ -1459,7 +1462,7 @@ class MessageLookup extends MessageLookupByLibrary { "playOnTv": MessageLookupByLibrary.simpleMessage("تشغيل الألبوم على التلفزيون"), "playOriginal": MessageLookupByLibrary.simpleMessage("تشغيل الأصلي"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("تشغيل البث"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("اشتراك متجر Play"), @@ -1472,14 +1475,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "يرجى الاتصال بالدعم إذا استمرت المشكلة."), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": - MessageLookupByLibrary.simpleMessage("يرجى منح الأذونات."), + MessageLookupByLibrary.simpleMessage("يرجى منح الأذونات"), "pleaseLoginAgain": - MessageLookupByLibrary.simpleMessage("يرجى تسجيل الدخول مرة أخرى."), + MessageLookupByLibrary.simpleMessage("يرجى تسجيل الدخول مرة أخرى"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "يرجى تحديد الروابط السريعة للإزالة."), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("يرجى المحاولة مرة أخرى"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1490,10 +1493,10 @@ class MessageLookup extends MessageLookupByLibrary { "يرجى الانتظار، جارٍ حذف الألبوم"), "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( - "يرجى الانتظار لبعض الوقت قبل إعادة المحاولة."), + "يرجى الانتظار لبعض الوقت قبل إعادة المحاولة"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "يرجى الانتظار، قد يستغرق هذا بعض الوقت."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("جارٍ تحضير السجلات..."), "preserveMore": MessageLookupByLibrary.simpleMessage("حفظ المزيد"), @@ -1511,7 +1514,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("متابعة"), "processed": MessageLookupByLibrary.simpleMessage("تمت المعالجة"), "processing": MessageLookupByLibrary.simpleMessage("المعالجة"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("معالجة مقاطع الفيديو"), "publicLinkCreated": @@ -1524,12 +1527,14 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("فتح تذكرة دعم"), "rateTheApp": MessageLookupByLibrary.simpleMessage("تقييم التطبيق"), "rateUs": MessageLookupByLibrary.simpleMessage("تقييم التطبيق"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("إعادة تعيين \"أنا\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("جارٍ إعادة التعيين..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("استعادة"), "recoverAccount": MessageLookupByLibrary.simpleMessage("استعادة الحساب"), @@ -1538,7 +1543,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("استرداد الحساب"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("بدء الاسترداد"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("مفتاح الاسترداد"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "تم نسخ مفتاح الاسترداد إلى الحافظة"), @@ -1549,15 +1554,15 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryKeySuccessBody": MessageLookupByLibrary.simpleMessage( "مفتاح الاسترداد الخاص بك صالح. شكرًا على التحقق.\n\nيرجى تذكر الاحتفاظ بنسخة احتياطية آمنة من مفتاح الاسترداد."), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( - "تم التحقق من مفتاح الاسترداد."), + "تم التحقق من مفتاح الاسترداد"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "مفتاح الاسترداد هو الطريقة الوحيدة لاستعادة صورك إذا نسيت كلمة المرور. يمكنك العثور عليه في الإعدادات > الحساب.\n\nالرجاء إدخال مفتاح الاسترداد هنا للتحقق من أنك حفظته بشكل صحيح."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("تم الاسترداد بنجاح!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "جهة اتصال موثوقة تحاول الوصول إلى حسابك"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "لا يمكن التحقق من كلمة المرور على جهازك الحالي، لكن يمكننا تعديلها لتعمل على جميع الأجهزة.\n\nسجّل الدخول باستخدام مفتاح الاسترداد، ثم أنشئ كلمة مرور جديدة (يمكنك اختيار نفس الكلمة السابقة إذا أردت)."), "recreatePasswordTitle": @@ -1573,13 +1578,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("1. أعطِ هذا الرمز لأصدقائك"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. يشتركون في خطة مدفوعة"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("الإحالات"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("الإحالات متوقفة مؤقتًا"), "rejectRecovery": MessageLookupByLibrary.simpleMessage("رفض الاسترداد"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( - "تذكر أيضًا إفراغ \"المحذوفة مؤخرًا\" من \"الإعدادات\" -> \"التخزين\" لاستعادة المساحة المحررة."), + "تذكر أيضًا إفراغ \"المحذوفة مؤخرًا\" من \"الإعدادات\" -> \"التخزين\" لاستعادة المساحة المحررة"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( "تذكر أيضًا إفراغ \"سلة المهملات\" لاستعادة المساحة المحررة."), "remoteImages": MessageLookupByLibrary.simpleMessage("الصور عن بعد"), @@ -1602,7 +1607,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("إزالة الرابط"), "removeParticipant": MessageLookupByLibrary.simpleMessage("إزالة المشارك"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("إزالة تسمية الشخص"), "removePublicLink": @@ -1623,7 +1628,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("إعادة تسمية الملف"), "renewSubscription": MessageLookupByLibrary.simpleMessage("تجديد الاشتراك"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("الإبلاغ عن خطأ"), "reportBug": MessageLookupByLibrary.simpleMessage("الإبلاغ عن خطأ"), "resendEmail": MessageLookupByLibrary.simpleMessage( @@ -1649,7 +1654,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("مراجعة الاقتراحات"), "right": MessageLookupByLibrary.simpleMessage("يمين"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("تدوير"), "rotateLeft": MessageLookupByLibrary.simpleMessage("تدوير لليسار"), "rotateRight": MessageLookupByLibrary.simpleMessage("تدوير لليمين"), @@ -1663,7 +1668,7 @@ class MessageLookup extends MessageLookupByLibrary { "savePerson": MessageLookupByLibrary.simpleMessage("حفظ الشخص"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( - "احفظ مفتاح الاسترداد إذا لم تكن قد فعلت ذلك بالفعل."), + "احفظ مفتاح الاسترداد إذا لم تكن قد فعلت ذلك"), "saving": MessageLookupByLibrary.simpleMessage("جارٍ الحفظ..."), "savingEdits": MessageLookupByLibrary.simpleMessage("جارٍ حفظ التعديلات..."), @@ -1703,7 +1708,8 @@ class MessageLookup extends MessageLookupByLibrary { "ادعُ الأشخاص، وسترى جميع الصور التي شاركوها هنا."), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "سيتم عرض الأشخاص هنا بمجرد اكتمال المعالجة والمزامنة."), - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("الأمان"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "رؤية روابط الألبومات العامة في التطبيق"), @@ -1748,9 +1754,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "سيتم إزالة العناصر المحددة من هذا الشخص، ولكن لن يتم حذفها من مكتبتك."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("إرسال"), "sendEmail": MessageLookupByLibrary.simpleMessage("إرسال بريد إلكتروني"), @@ -1780,16 +1786,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("شارك ألبومًا الآن"), "shareLink": MessageLookupByLibrary.simpleMessage("مشاركة الرابط"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "شارك فقط مع الأشخاص الذين تريدهم."), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "قم بتنزيل تطبيق Ente حتى نتمكن من مشاركة الصور ومقاطع الفيديو بالجودة الأصلية بسهولة.\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "المشاركة مع غير مستخدمي Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("شارك ألبومك الأول"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1802,7 +1808,7 @@ class MessageLookup extends MessageLookupByLibrary { "إشعارات الصور المشتركة الجديدة"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "تلقّ إشعارات عندما يضيف شخص ما صورة إلى ألبوم مشترك أنت جزء منه."), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("تمت مشاركتها معي"), "sharedWithYou": @@ -1820,11 +1826,11 @@ class MessageLookup extends MessageLookupByLibrary { "تسجيل الخروج من الأجهزة الأخرى"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "أوافق على شروط الخدمة وسياسة الخصوصية"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "سيتم حذفه من جميع الألبومات."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("تخط"), "social": MessageLookupByLibrary.simpleMessage("التواصل الاجتماعي"), "someItemsAreInBothEnteAndYourDevice": @@ -1842,6 +1848,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "حدث خطأ ما، يرجى المحاولة مرة أخرى"), "sorry": MessageLookupByLibrary.simpleMessage("عفوًا"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "عذرًا، لم نتمكن من عمل نسخة احتياطية لهذا الملف الآن، سنعيد المحاولة لاحقًا."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "عذرًا، تعذرت الإضافة إلى المفضلة!"), "sorryCouldNotRemoveFromFavorites": @@ -1853,13 +1861,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "عذرًا، لم نتمكن من إنشاء مفاتيح آمنة على هذا الجهاز.\n\nيرجى التسجيل من جهاز مختلف."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("فرز"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("فرز حسب"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("الأحدث أولاً"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("الأقدم أولاً"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ نجاح"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("تسليط الضوء عليك"), "startAccountRecoveryTitle": @@ -1873,14 +1883,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("التخزين"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("العائلة"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("أنت"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": - MessageLookupByLibrary.simpleMessage("تم تجاوز حد التخزين."), - "storageUsageInfo": m90, + MessageLookupByLibrary.simpleMessage("تم تجاوز حد التخزين"), + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("تفاصيل البث"), "strongStrength": MessageLookupByLibrary.simpleMessage("قوية"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("اشتراك"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "المشاركة متاحة فقط للاشتراكات المدفوعة النشطة."), @@ -1897,7 +1907,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("اقتراح ميزة"), "sunrise": MessageLookupByLibrary.simpleMessage("على الأفق"), "support": MessageLookupByLibrary.simpleMessage("الدعم"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("توقفت المزامنة"), "syncing": MessageLookupByLibrary.simpleMessage("جارٍ المزامنة..."), "systemTheme": MessageLookupByLibrary.simpleMessage("النظام"), @@ -1906,7 +1916,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("انقر لإدخال الرمز"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("انقر لفتح القفل"), "tapToUpload": MessageLookupByLibrary.simpleMessage("انقر للتحميل"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "يبدو أن خطأً ما قد حدث. يرجى المحاولة مرة أخرى بعد بعض الوقت. إذا استمر الخطأ، يرجى الاتصال بفريق الدعم لدينا."), "terminate": MessageLookupByLibrary.simpleMessage("إنهاء"), @@ -1919,7 +1929,7 @@ class MessageLookup extends MessageLookupByLibrary { "thankYouForSubscribing": MessageLookupByLibrary.simpleMessage("شكرًا لاشتراكك!"), "theDownloadCouldNotBeCompleted": - MessageLookupByLibrary.simpleMessage("تعذر إكمال التنزيل."), + MessageLookupByLibrary.simpleMessage("تعذر إكمال التنزيل"), "theLinkYouAreTryingToAccessHasExpired": MessageLookupByLibrary.simpleMessage( "انتهت صلاحية الرابط الذي تحاول الوصول إليه."), @@ -1930,7 +1940,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "سيتم حذف هذه العناصر من جهازك."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "سيتم حذفها من جميع الألبومات."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1940,31 +1950,31 @@ class MessageLookup extends MessageLookupByLibrary { "هذا الألبوم لديه رابط تعاوني بالفعل."), "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( - "يمكن استخدام هذا المفتاح لاستعادة حسابك إذا فقدت جهاز المصادقة الثنائية."), + "يمكن استخدام هذا المفتاح لاستعادة حسابك إذا فقدت العامل الثاني للمصادقة"), "thisDevice": MessageLookupByLibrary.simpleMessage("هذا الجهاز"), "thisEmailIsAlreadyInUse": MessageLookupByLibrary.simpleMessage( "هذا البريد الإلكتروني مستخدم بالفعل."), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "لا تحتوي هذه الصورة على بيانات EXIF."), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("هذا أنا!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "هذا هو معرّف التحقق الخاص بك"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("هذا الأسبوع عبر السنين"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "سيؤدي هذا إلى تسجيل خروجك من الجهاز التالي:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( - "سيؤدي هذا إلى تسجيل خروجك من هذا الجهاز."), + "سيؤدي هذا إلى تسجيل خروجك من هذا الجهاز!"), "thisWillMakeTheDateAndTimeOfAllSelected": MessageLookupByLibrary.simpleMessage( "سيجعل هذا تاريخ ووقت جميع الصور المحددة متماثلاً."), "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "سيؤدي هذا إلى إزالة الروابط العامة لجميع الروابط السريعة المحددة."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "لتمكين قفل التطبيق، يرجى إعداد رمز مرور الجهاز أو قفل الشاشة في إعدادات النظام."), @@ -1978,13 +1988,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("المجموع"), "totalSize": MessageLookupByLibrary.simpleMessage("الحجم الإجمالي"), "trash": MessageLookupByLibrary.simpleMessage("سلة المهملات"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("قص"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("جهات الاتصال الموثوقة"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("المحاولة مرة أخرى"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "قم بتشغيل النسخ الاحتياطي لتحميل الملفات المضافة إلى مجلد الجهاز هذا تلقائيًا إلى Ente."), @@ -2001,7 +2011,7 @@ class MessageLookup extends MessageLookupByLibrary { "تمت إعادة تعيين المصادقة الثنائية بنجاح."), "twofactorSetup": MessageLookupByLibrary.simpleMessage("إعداد المصادقة الثنائية"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("إلغاء الأرشفة"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("إلغاء أرشفة الألبوم"), @@ -2025,10 +2035,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("جارٍ تحديث تحديد المجلد..."), "upgrade": MessageLookupByLibrary.simpleMessage("ترقية"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "جارٍ تحميل الملفات إلى الألبوم..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("جارٍ حفظ ذكرى واحدة..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2046,7 +2056,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("استخدام الصورة المحددة"), "usedSpace": MessageLookupByLibrary.simpleMessage("المساحة المستخدمة"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "فشل التحقق، يرجى المحاولة مرة أخرى."), @@ -2054,7 +2064,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("التحقق"), "verifyEmail": MessageLookupByLibrary.simpleMessage("التحقق من البريد الإلكتروني"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("تحقق"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("التحقق من مفتاح المرور"), @@ -2065,7 +2075,8 @@ class MessageLookup extends MessageLookupByLibrary { "جارٍ التحقق من مفتاح الاسترداد..."), "videoInfo": MessageLookupByLibrary.simpleMessage("معلومات الفيديو"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("فيديو"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("بث الفيديو"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("مقاطع فيديو قابلة للبث"), "videos": MessageLookupByLibrary.simpleMessage("مقاطع الفيديو"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("عرض الجلسات النشطة"), @@ -2078,10 +2089,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "عرض الملفات التي تستهلك أكبر قدر من مساحة التخزين."), "viewLogs": MessageLookupByLibrary.simpleMessage("عرض السجلات"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("عرض مفتاح الاسترداد"), "viewer": MessageLookupByLibrary.simpleMessage("مشاهد"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "يرجى زيارة web.ente.io لإدارة اشتراكك."), "waitingForVerification": @@ -2094,7 +2106,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "لا ندعم تعديل الصور والألبومات التي لا تملكها بعد."), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("ضعيفة"), "welcomeBack": MessageLookupByLibrary.simpleMessage("أهلاً بعودتك!"), "whatsNew": MessageLookupByLibrary.simpleMessage("ما الجديد"), @@ -2102,7 +2114,7 @@ class MessageLookup extends MessageLookupByLibrary { "يمكن لجهة الاتصال الموثوقة المساعدة في استعادة بياناتك."), "yearShort": MessageLookupByLibrary.simpleMessage("سنة"), "yearly": MessageLookupByLibrary.simpleMessage("سنويًا"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("نعم"), "yesCancel": MessageLookupByLibrary.simpleMessage("نعم، إلغاء"), "yesConvertToViewer": @@ -2116,7 +2128,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("نعم، إعادة تعيين الشخص"), "you": MessageLookupByLibrary.simpleMessage("أنت"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("أنت مشترك في خطة عائلية!"), "youAreOnTheLatestVersion": @@ -2135,9 +2147,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("لا يمكنك المشاركة مع نفسك."), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "لا توجد لديك أي عناصر مؤرشفة."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": - MessageLookupByLibrary.simpleMessage("تم حذف حسابك بنجاح."), + MessageLookupByLibrary.simpleMessage("تم حذف حسابك بنجاح"), "yourMap": MessageLookupByLibrary.simpleMessage("خريطتك"), "yourPlanWasSuccessfullyDowngraded": MessageLookupByLibrary.simpleMessage("تم تخفيض خطتك بنجاح."), @@ -2149,14 +2161,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "تعذر جلب تفاصيل التخزين الخاصة بك."), "yourSubscriptionHasExpired": - MessageLookupByLibrary.simpleMessage("انتهت صلاحية اشتراكك."), + MessageLookupByLibrary.simpleMessage("انتهت صلاحية اشتراكك"), "yourSubscriptionWasUpdatedSuccessfully": MessageLookupByLibrary.simpleMessage("تم تحديث اشتراكك بنجاح."), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "انتهت صلاحية رمز التحقق الخاص بك."), "youveNoDuplicateFilesThatCanBeCleared": MessageLookupByLibrary.simpleMessage( - "لا توجد لديك أي ملفات مكررة يمكن مسحها."), + "لا توجد لديك أي ملفات مكررة يمكن مسحها"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "لا توجد لديك ملفات في هذا الألبوم يمكن حذفها."), diff --git a/mobile/lib/generated/intl/messages_be.dart b/mobile/lib/generated/intl/messages_be.dart index c913d23fbc..68aa60a736 100644 --- a/mobile/lib/generated/intl/messages_be.dart +++ b/mobile/lib/generated/intl/messages_be.dart @@ -20,12 +20,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'be'; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надзейнасць пароля: ${passwordStrengthValue}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} Гб"; + static String m91(storageAmountInGB) => "${storageAmountInGB} Гб"; - static String m109(email) => + static String m112(email) => "Ліст адпраўлены на электронную пошту ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -49,6 +49,7 @@ class MessageLookup extends MessageLookupByLibrary { "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Якая асноўная прычына выдалення вашага ўліковага запісу?"), "backup": MessageLookupByLibrary.simpleMessage("Рэзервовая копія"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "cancel": MessageLookupByLibrary.simpleMessage("Скасаваць"), "change": MessageLookupByLibrary.simpleMessage("Змяніць"), "changeEmail": MessageLookupByLibrary.simpleMessage( @@ -57,6 +58,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Змяніць пароль"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Праверце свае ўваходныя лісты (і спам) для завяршэння праверкі"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Код ужыты"), "confirm": MessageLookupByLibrary.simpleMessage("Пацвердзіць"), @@ -141,6 +144,8 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Асноўныя"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Генерацыя ключоў шыфравання..."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "howItWorks": MessageLookupByLibrary.simpleMessage("Як гэта працуе"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Iгнараваць"), "incorrectPasswordTitle": @@ -167,12 +172,15 @@ class MessageLookup extends MessageLookupByLibrary { "loginTerms": MessageLookupByLibrary.simpleMessage( "Націскаючы ўвайсці, я пагаджаюся з умовамі абслугоўвання і палітыкай прыватнасці"), "logout": MessageLookupByLibrary.simpleMessage("Выйсці"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("Згубілі прыладу?"), "magicSearch": MessageLookupByLibrary.simpleMessage("Магічны пошук"), "manage": MessageLookupByLibrary.simpleMessage("Кіраванне"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Кіраванне"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Умераны"), "never": MessageLookupByLibrary.simpleMessage("Ніколі"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ Няма дублікатаў"), "noRecoveryKey": @@ -181,13 +189,16 @@ class MessageLookup extends MessageLookupByLibrary { "Вашы даныя не могуць быць расшыфраваны без пароля або ключа аднаўлення па прычыне архітэктуры наша пратакола скразнога шыфравання"), "notifications": MessageLookupByLibrary.simpleMessage("Апавяшчэнні"), "ok": MessageLookupByLibrary.simpleMessage("Добра"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("Вой"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Штосьці пайшло не так"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Пароль паспяхова зменены"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Мы не захоўваем гэты пароль і мы не зможам расшыфраваць вашы даныя, калі вы забудзеце яго"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("фота"), @@ -197,6 +208,8 @@ class MessageLookup extends MessageLookupByLibrary { "privacyPolicyTitle": MessageLookupByLibrary.simpleMessage("Палітыка прыватнасці"), "rateUs": MessageLookupByLibrary.simpleMessage("Ацаніце нас"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Аднавіць"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Аднавіць уліковы запіс"), @@ -248,8 +261,10 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Немагчыма згенерыраваць ключы бяспекі на гэтай прыладзе.\n\nЗарэгіструйцеся з іншай прылады."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "status": MessageLookupByLibrary.simpleMessage("Стан"), - "storageInGB": m89, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Надзейны"), "support": MessageLookupByLibrary.simpleMessage("Падтрымка"), "systemTheme": MessageLookupByLibrary.simpleMessage("Сістэма"), @@ -288,7 +303,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoSmallCase": MessageLookupByLibrary.simpleMessage("відэа"), "viewLargeFiles": MessageLookupByLibrary.simpleMessage("Вялікія файлы"), "viewer": MessageLookupByLibrary.simpleMessage("Праглядальнік"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Ненадзейны"), "welcomeBack": MessageLookupByLibrary.simpleMessage("З вяртаннем!"), "yesDelete": MessageLookupByLibrary.simpleMessage("Так, выдаліць"), diff --git a/mobile/lib/generated/intl/messages_bg.dart b/mobile/lib/generated/intl/messages_bg.dart index e887127f40..636f6859e6 100644 --- a/mobile/lib/generated/intl/messages_bg.dart +++ b/mobile/lib/generated/intl/messages_bg.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'bg'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_ca.dart b/mobile/lib/generated/intl/messages_ca.dart index 84dea987b0..6dabe20ccc 100644 --- a/mobile/lib/generated/intl/messages_ca.dart +++ b/mobile/lib/generated/intl/messages_ca.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ca'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 20f003b5bd..3fe9fedc3b 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -20,19 +20,43 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'cs'; + static String m6(albumName) => "Úspěšně přidáno do ${albumName}"; + + static String m46(expiryTime) => "Platnost odkazu vyprší ${expiryTime}"; + + static String m67(storeName) => "Ohodnoťte nás na ${storeName}"; + + static String m74(endDate) => "Předplatné se obnoví ${endDate}"; + + static String m80(name) => "Selfie s ${name}"; + + static String m109(email) => "Ověřit ${email}"; + + static String m114(name) => "Vy a ${name}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage( + "Je dostupná nová verze Ente."), "acceptTrustInvite": MessageLookupByLibrary.simpleMessage("Přijmout pozvání"), "account": MessageLookupByLibrary.simpleMessage("Účet"), "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Vítejte zpět!"), + "activeSessions": + MessageLookupByLibrary.simpleMessage("Aktivní relace"), "add": MessageLookupByLibrary.simpleMessage("Přidat"), + "addFiles": MessageLookupByLibrary.simpleMessage("Přidat soubory"), + "addLocation": MessageLookupByLibrary.simpleMessage("Přidat polohu"), + "addLocationButton": MessageLookupByLibrary.simpleMessage("Přidat"), + "addMore": MessageLookupByLibrary.simpleMessage("Přidat další"), "addName": MessageLookupByLibrary.simpleMessage("Přidat název"), "addNew": MessageLookupByLibrary.simpleMessage("Přidat nový"), "addNewPerson": MessageLookupByLibrary.simpleMessage("Přidat novou osobu"), + "addPhotos": MessageLookupByLibrary.simpleMessage("Přidat fotky"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Přidat do alba"), + "addedSuccessfullyTo": m6, "advancedSettings": MessageLookupByLibrary.simpleMessage("Pokročilé"), "after1Day": MessageLookupByLibrary.simpleMessage("Po 1 dni"), "after1Hour": MessageLookupByLibrary.simpleMessage("Po 1 hodině"), @@ -40,99 +64,237 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Po 1 týdnu"), "after1Year": MessageLookupByLibrary.simpleMessage("Po 1 roce"), "albumOwner": MessageLookupByLibrary.simpleMessage("Vlastník"), + "albumUpdated": + MessageLookupByLibrary.simpleMessage("Album bylo aktualizováno"), + "albums": MessageLookupByLibrary.simpleMessage("Alba"), "allow": MessageLookupByLibrary.simpleMessage("Povolit"), + "androidBiometricRequiredTitle": MessageLookupByLibrary.simpleMessage( + "Je požadováno biometrické ověření"), + "androidBiometricSuccess": + MessageLookupByLibrary.simpleMessage("Úspěšně dokončeno"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("Zrušit"), "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Použít"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Archivovat album"), "archiving": MessageLookupByLibrary.simpleMessage("Archivování..."), + "areYouSureYouWantToLogout": + MessageLookupByLibrary.simpleMessage("Opravdu se chcete odhlásit?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Jaký je váš hlavní důvod, proč mažete svůj účet?"), "autoLock": MessageLookupByLibrary.simpleMessage("Automatické zamykání"), + "backedUpFolders": + MessageLookupByLibrary.simpleMessage("Zálohované složky"), + "backup": MessageLookupByLibrary.simpleMessage("Zálohovat"), + "backupFile": MessageLookupByLibrary.simpleMessage("Zálohovat soubor"), + "backupStatus": MessageLookupByLibrary.simpleMessage("Stav zálohování"), "birthday": MessageLookupByLibrary.simpleMessage("Narozeniny"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nová ikona"), - "cLMemories": MessageLookupByLibrary.simpleMessage("Vzpomínky"), + "cachedData": + MessageLookupByLibrary.simpleMessage("Data uložená v mezipaměti"), + "calculating": + MessageLookupByLibrary.simpleMessage("Probíhá výpočet..."), "cancel": MessageLookupByLibrary.simpleMessage("Zrušit"), + "cancelAccountRecovery": + MessageLookupByLibrary.simpleMessage("Zrušit obnovení"), + "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( + "Sdílené soubory nelze odstranit"), "changeEmail": MessageLookupByLibrary.simpleMessage("Změnit e-mail"), + "changePassword": MessageLookupByLibrary.simpleMessage("Změnit heslo"), + "checkForUpdates": + MessageLookupByLibrary.simpleMessage("Zkontrolovat aktualizace"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Zkontrolujte prosím svou doručenou poštu (a spam) pro dokončení ověření"), + "checkStatus": + MessageLookupByLibrary.simpleMessage("Zkontrolovat stav"), + "checking": MessageLookupByLibrary.simpleMessage("Probíhá kontrola..."), + "clearCaches": + MessageLookupByLibrary.simpleMessage("Vymazat mezipaměť"), + "clearIndexes": MessageLookupByLibrary.simpleMessage("Smazat indexy"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Zavřít"), + "codeAppliedPageTitle": + MessageLookupByLibrary.simpleMessage("Kód byl použit"), "collaborator": MessageLookupByLibrary.simpleMessage("Spolupracovník"), "collageLayout": MessageLookupByLibrary.simpleMessage("Rozvržení"), "color": MessageLookupByLibrary.simpleMessage("Barva"), + "configuration": MessageLookupByLibrary.simpleMessage("Nastavení"), "confirm": MessageLookupByLibrary.simpleMessage("Potvrdit"), + "confirmAccountDeletion": + MessageLookupByLibrary.simpleMessage("Potvrdit odstranění účtu"), + "contactSupport": + MessageLookupByLibrary.simpleMessage("Kontaktovat podporu"), "contacts": MessageLookupByLibrary.simpleMessage("Kontakty"), "continueLabel": MessageLookupByLibrary.simpleMessage("Pokračovat"), + "count": MessageLookupByLibrary.simpleMessage("Počet"), + "crashReporting": + MessageLookupByLibrary.simpleMessage("Hlášení o pádu"), "create": MessageLookupByLibrary.simpleMessage("Vytvořit"), "createAccount": MessageLookupByLibrary.simpleMessage("Vytvořit účet"), "createNewAccount": MessageLookupByLibrary.simpleMessage("Vytvořit nový účet"), "crop": MessageLookupByLibrary.simpleMessage("Oříznout"), + "darkTheme": MessageLookupByLibrary.simpleMessage("Tmavý"), "dayToday": MessageLookupByLibrary.simpleMessage("Dnes"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Včera"), "declineTrustInvite": MessageLookupByLibrary.simpleMessage("Odmítnout pozvání"), "decrypting": MessageLookupByLibrary.simpleMessage("Dešifrování..."), + "decryptingVideo": + MessageLookupByLibrary.simpleMessage("Dešifrování videa..."), "delete": MessageLookupByLibrary.simpleMessage("Smazat"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Smazat účet"), + "deleteAccountPermanentlyButton": + MessageLookupByLibrary.simpleMessage("Trvale smazat účet"), + "deleteAlbum": MessageLookupByLibrary.simpleMessage("Odstranit album"), + "deleteAll": MessageLookupByLibrary.simpleMessage("Smazat vše"), "deleteEmptyAlbums": MessageLookupByLibrary.simpleMessage("Smazat prázdná alba"), "deleteEmptyAlbumsWithQuestionMark": MessageLookupByLibrary.simpleMessage("Smazat prázdná alba?"), + "deleteFromBoth": + MessageLookupByLibrary.simpleMessage("Odstranit z obou"), + "deleteFromDevice": + MessageLookupByLibrary.simpleMessage("Odstranit ze zařízení"), + "deleteFromEnte": + MessageLookupByLibrary.simpleMessage("Odstranit z Ente"), + "deleteLocation": + MessageLookupByLibrary.simpleMessage("Odstranit polohu"), + "deleteReason1": MessageLookupByLibrary.simpleMessage( + "Chybí klíčová funkce, kterou potřebuji"), + "deleteReason2": MessageLookupByLibrary.simpleMessage( + "Aplikace nebo určitá funkce se nechová tak, jak si myslím, že by měla"), + "deleteReason3": MessageLookupByLibrary.simpleMessage( + "Našel jsem jinou službu, která se mi líbí více"), + "deleteReason4": + MessageLookupByLibrary.simpleMessage("Můj důvod není uveden"), + "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( + "Váš požadavek bude zpracován do 72 hodin."), + "deleteSharedAlbum": + MessageLookupByLibrary.simpleMessage("Opustit sdílené album?"), + "deselectAll": + MessageLookupByLibrary.simpleMessage("Zrušte výběr všech"), "deviceCodeHint": MessageLookupByLibrary.simpleMessage("Zadejte kód"), + "deviceLock": MessageLookupByLibrary.simpleMessage("Zámek zařízení"), + "deviceNotFound": + MessageLookupByLibrary.simpleMessage("Zařízení nebylo nalezeno"), + "didYouKnow": MessageLookupByLibrary.simpleMessage("Věděli jste?"), "discord": MessageLookupByLibrary.simpleMessage("Discord"), "discover_food": MessageLookupByLibrary.simpleMessage("Jídlo"), "discover_pets": MessageLookupByLibrary.simpleMessage("Domácí mazlíčci"), + "discover_screenshots": + MessageLookupByLibrary.simpleMessage("Snímky obrazovky"), "discover_selfies": MessageLookupByLibrary.simpleMessage("Selfie"), "discover_wallpapers": MessageLookupByLibrary.simpleMessage("Pozadí"), "dismiss": MessageLookupByLibrary.simpleMessage("Zrušit"), "done": MessageLookupByLibrary.simpleMessage("Hotovo"), + "doubleYourStorage": + MessageLookupByLibrary.simpleMessage("Zdvojnásobte své úložiště"), "download": MessageLookupByLibrary.simpleMessage("Stáhnout"), + "downloadFailed": + MessageLookupByLibrary.simpleMessage("Stahování selhalo"), + "downloading": MessageLookupByLibrary.simpleMessage("Stahuji..."), "edit": MessageLookupByLibrary.simpleMessage("Upravit"), + "editLocation": MessageLookupByLibrary.simpleMessage("Upravit polohu"), + "editLocationTagTitle": + MessageLookupByLibrary.simpleMessage("Upravit lokalitu"), "editPerson": MessageLookupByLibrary.simpleMessage("Upravit osobu"), "editTime": MessageLookupByLibrary.simpleMessage("Upravit čas"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "E-mail je již zaregistrován."), + "emailNotRegistered": + MessageLookupByLibrary.simpleMessage("E-mail není registrován."), "empty": MessageLookupByLibrary.simpleMessage("Vyprázdnit"), + "emptyTrash": MessageLookupByLibrary.simpleMessage("Vyprázdnit koš?"), "enable": MessageLookupByLibrary.simpleMessage("Povolit"), + "enabled": MessageLookupByLibrary.simpleMessage("Zapnuto"), "encryption": MessageLookupByLibrary.simpleMessage("Šifrování"), + "encryptionKeys": + MessageLookupByLibrary.simpleMessage("Šifrovací klíče"), + "enterAlbumName": + MessageLookupByLibrary.simpleMessage("Zadejte název alba"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("Narozeniny (volitelné)"), + "enterFileName": + MessageLookupByLibrary.simpleMessage("Zadejte název souboru"), + "enterPassword": MessageLookupByLibrary.simpleMessage("Zadejte heslo"), + "enterPin": MessageLookupByLibrary.simpleMessage("Zadejte PIN"), "enterValidEmail": MessageLookupByLibrary.simpleMessage( "Prosím, zadejte platnou e-mailovou adresu."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Zadejte svou e-mailovou adresu"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Zadejte své heslo"), + "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Zadejte svůj obnovovací klíč"), + "everywhere": MessageLookupByLibrary.simpleMessage("všude"), "exif": MessageLookupByLibrary.simpleMessage("EXIF"), + "exportLogs": MessageLookupByLibrary.simpleMessage("Exportovat logy"), + "exportYourData": + MessageLookupByLibrary.simpleMessage("Exportujte svá data"), "faces": MessageLookupByLibrary.simpleMessage("Obličeje"), "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage( "Stahování videa se nezdařilo"), + "failedToLoadAlbums": + MessageLookupByLibrary.simpleMessage("Nepodařilo se načíst alba"), + "failedToPlayVideo": MessageLookupByLibrary.simpleMessage( + "Přehrávání videa se nezdařilo"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("Rodina"), "faq": MessageLookupByLibrary.simpleMessage("Často kladené dotazy"), + "faqs": MessageLookupByLibrary.simpleMessage("Často kladené dotazy"), "favorite": MessageLookupByLibrary.simpleMessage("Oblíbené"), "feedback": MessageLookupByLibrary.simpleMessage("Zpětná vazba"), + "fileInfoAddDescHint": + MessageLookupByLibrary.simpleMessage("Přidat popis..."), + "fileTypes": MessageLookupByLibrary.simpleMessage("Typy souboru"), + "filesDeleted": + MessageLookupByLibrary.simpleMessage("Soubory odstraněny"), "flip": MessageLookupByLibrary.simpleMessage("Překlopit"), + "freeUpSpace": MessageLookupByLibrary.simpleMessage("Uvolnit místo"), "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "general": MessageLookupByLibrary.simpleMessage("Obecné"), + "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( + "Generování šifrovacích klíčů..."), + "goToSettings": + MessageLookupByLibrary.simpleMessage("Jít do nastavení"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "hidden": MessageLookupByLibrary.simpleMessage("Skryté"), "hide": MessageLookupByLibrary.simpleMessage("Skrýt"), + "howItWorks": MessageLookupByLibrary.simpleMessage("Jak to funguje"), "iOSOkButton": MessageLookupByLibrary.simpleMessage("OK"), + "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignorovat"), "immediately": MessageLookupByLibrary.simpleMessage("Ihned"), + "importing": MessageLookupByLibrary.simpleMessage("Importování…"), + "incorrectPasswordTitle": + MessageLookupByLibrary.simpleMessage("Nesprávné heslo"), "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage(""), + "incorrectRecoveryKeyTitle": + MessageLookupByLibrary.simpleMessage("Nesprávný obnovovací klíč"), "info": MessageLookupByLibrary.simpleMessage("Informace"), + "installManually": + MessageLookupByLibrary.simpleMessage("Instalovat manuálně"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Neplatná e-mailová adresa"), "invalidKey": MessageLookupByLibrary.simpleMessage("Neplatný klíč"), "invite": MessageLookupByLibrary.simpleMessage("Pozvat"), + "inviteToEnte": MessageLookupByLibrary.simpleMessage("Pozvat do Ente"), "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage("Pozvěte své přátelé do Ente"), + "join": MessageLookupByLibrary.simpleMessage("Připojit se"), + "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( + "Pomozte nám s těmito informacemi"), "language": MessageLookupByLibrary.simpleMessage("Jazyk"), "leave": MessageLookupByLibrary.simpleMessage("Odejít"), "leaveAlbum": MessageLookupByLibrary.simpleMessage("Opustit album"), "left": MessageLookupByLibrary.simpleMessage("Doleva"), + "lightTheme": MessageLookupByLibrary.simpleMessage("Světlý"), + "linkExpiresOn": m46, "linkHasExpired": MessageLookupByLibrary.simpleMessage("Platnost odkazu vypršela"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nikdy"), @@ -140,13 +302,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Načítání galerie..."), "location": MessageLookupByLibrary.simpleMessage("Poloha"), "locations": MessageLookupByLibrary.simpleMessage("Lokality"), + "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Uzamknout"), + "logInLabel": MessageLookupByLibrary.simpleMessage("Přihlásit se"), "loggingOut": MessageLookupByLibrary.simpleMessage("Odhlašování..."), "loginSessionExpired": MessageLookupByLibrary.simpleMessage("Relace vypršela"), + "loginWithTOTP": + MessageLookupByLibrary.simpleMessage("Přihlášení pomocí TOTP"), "logout": MessageLookupByLibrary.simpleMessage("Odhlásit se"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("Ztratili jste zařízení?"), "manage": MessageLookupByLibrary.simpleMessage("Spravovat"), + "manageParticipants": MessageLookupByLibrary.simpleMessage("Spravovat"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Spravovat předplatné"), "map": MessageLookupByLibrary.simpleMessage("Mapa"), @@ -154,13 +323,22 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Já"), + "merchandise": MessageLookupByLibrary.simpleMessage("E-shop"), + "mergedPhotos": + MessageLookupByLibrary.simpleMessage("Sloučené fotografie"), + "moments": MessageLookupByLibrary.simpleMessage("Momenty"), "monthly": MessageLookupByLibrary.simpleMessage("Měsíčně"), "moreDetails": MessageLookupByLibrary.simpleMessage("Další podrobnosti"), + "moveToAlbum": + MessageLookupByLibrary.simpleMessage("Přesunout do alba"), "movedToTrash": MessageLookupByLibrary.simpleMessage("Přesunuto do koše"), "never": MessageLookupByLibrary.simpleMessage("Nikdy"), "newAlbum": MessageLookupByLibrary.simpleMessage("Nové album"), + "newPerson": MessageLookupByLibrary.simpleMessage("Nová osoba"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "newRange": MessageLookupByLibrary.simpleMessage("Nový rozsah"), "newest": MessageLookupByLibrary.simpleMessage("Nejnovější"), "next": MessageLookupByLibrary.simpleMessage("Další"), "no": MessageLookupByLibrary.simpleMessage("Ne"), @@ -170,86 +348,209 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("✨ Žádné duplicity"), "noInternetConnection": MessageLookupByLibrary.simpleMessage("Žádné připojení k internetu"), + "noPhotosFoundHere": MessageLookupByLibrary.simpleMessage( + "Zde nebyly nalezeny žádné fotky"), + "noRecoveryKey": + MessageLookupByLibrary.simpleMessage("Nemáte obnovovací klíč?"), "noResultsFound": MessageLookupByLibrary.simpleMessage( "Nebyly nalezeny žádné výsledky"), "notifications": MessageLookupByLibrary.simpleMessage("Notifikace"), "ok": MessageLookupByLibrary.simpleMessage("Ok"), + "onDevice": MessageLookupByLibrary.simpleMessage("V zařízení"), + "onEnte": MessageLookupByLibrary.simpleMessage( + "Na ente"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "oops": MessageLookupByLibrary.simpleMessage("Jejda"), + "oopsSomethingWentWrong": + MessageLookupByLibrary.simpleMessage("Jejda, něco se pokazilo"), + "openAlbumInBrowser": + MessageLookupByLibrary.simpleMessage("Otevřít album v prohlížeči"), "openFile": MessageLookupByLibrary.simpleMessage("Otevřít soubor"), + "openSettings": + MessageLookupByLibrary.simpleMessage("Otevřít Nastavení"), + "pair": MessageLookupByLibrary.simpleMessage("Spárovat"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), "password": MessageLookupByLibrary.simpleMessage("Heslo"), + "passwordChangedSuccessfully": + MessageLookupByLibrary.simpleMessage("Heslo úspěšně změněno"), + "paymentDetails": + MessageLookupByLibrary.simpleMessage("Platební údaje"), "people": MessageLookupByLibrary.simpleMessage("Lidé"), + "permanentlyDelete": + MessageLookupByLibrary.simpleMessage("Trvale odstranit"), "personName": MessageLookupByLibrary.simpleMessage("Jméno osoby"), + "photos": MessageLookupByLibrary.simpleMessage("Fotky"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Připnout album"), + "pleaseLoginAgain": + MessageLookupByLibrary.simpleMessage("Přihlaste se, prosím, znovu"), "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Zkuste to prosím znovu"), "pleaseWait": MessageLookupByLibrary.simpleMessage("Čekejte prosím..."), "previous": MessageLookupByLibrary.simpleMessage("Předchozí"), "privacy": MessageLookupByLibrary.simpleMessage("Soukromí"), + "processing": MessageLookupByLibrary.simpleMessage("Zpracovává se"), + "publicLinkCreated": + MessageLookupByLibrary.simpleMessage("Veřejný odkaz vytvořen"), + "queued": MessageLookupByLibrary.simpleMessage("Ve frontě"), + "radius": MessageLookupByLibrary.simpleMessage("Rádius"), "rateUs": MessageLookupByLibrary.simpleMessage("Ohodnoť nás"), + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "recoverButton": MessageLookupByLibrary.simpleMessage("Obnovit"), + "recoveryKeyVerified": + MessageLookupByLibrary.simpleMessage("Obnovovací klíč byl ověřen"), + "recoverySuccessful": + MessageLookupByLibrary.simpleMessage("Úspěšně obnoveno!"), + "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), + "reenterPin": MessageLookupByLibrary.simpleMessage("Zadejte PIN znovu"), "remove": MessageLookupByLibrary.simpleMessage("Odstranit"), "removeDuplicates": MessageLookupByLibrary.simpleMessage("Odstranit duplicity"), + "removeFromAlbum": + MessageLookupByLibrary.simpleMessage("Odstranit z alba"), + "removeFromAlbumTitle": + MessageLookupByLibrary.simpleMessage("Odstranit z alba?"), "removeInvite": MessageLookupByLibrary.simpleMessage("Odstranit pozvání"), "removeLink": MessageLookupByLibrary.simpleMessage("Odstranit odkaz"), + "removeParticipant": + MessageLookupByLibrary.simpleMessage("Odebrat účastníka"), + "removePublicLink": + MessageLookupByLibrary.simpleMessage("Odstranit veřejný odkaz"), + "removeWithQuestionMark": + MessageLookupByLibrary.simpleMessage("Odstranit?"), + "rename": MessageLookupByLibrary.simpleMessage("Přejmenovat"), "renameAlbum": MessageLookupByLibrary.simpleMessage("Přejmenovat album"), "renameFile": MessageLookupByLibrary.simpleMessage("Přejmenovat soubor"), + "renewsOn": m74, + "reportABug": MessageLookupByLibrary.simpleMessage("Nahlásit chybu"), + "reportBug": MessageLookupByLibrary.simpleMessage("Nahlásit chybu"), + "resendEmail": + MessageLookupByLibrary.simpleMessage("Znovu odeslat e-mail"), + "resetPasswordTitle": + MessageLookupByLibrary.simpleMessage("Obnovit heslo"), "resetPerson": MessageLookupByLibrary.simpleMessage("Odstranit"), "restore": MessageLookupByLibrary.simpleMessage("Obnovit"), + "restoringFiles": + MessageLookupByLibrary.simpleMessage("Obnovuji soubory..."), + "retry": MessageLookupByLibrary.simpleMessage("Opakovat"), "right": MessageLookupByLibrary.simpleMessage("Doprava"), "rotate": MessageLookupByLibrary.simpleMessage("Otočit"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Otočit doleva"), "rotateRight": MessageLookupByLibrary.simpleMessage("Otočit doprava"), + "safelyStored": + MessageLookupByLibrary.simpleMessage("Bezpečně uloženo"), "save": MessageLookupByLibrary.simpleMessage("Uložit"), "saveCopy": MessageLookupByLibrary.simpleMessage("Uložit kopii"), "saveKey": MessageLookupByLibrary.simpleMessage("Uložit klíč"), "savePerson": MessageLookupByLibrary.simpleMessage("Uložit osobu"), + "search": MessageLookupByLibrary.simpleMessage("Hledat"), "searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("Alba"), + "searchByAlbumNameHint": + MessageLookupByLibrary.simpleMessage("Název alba"), "security": MessageLookupByLibrary.simpleMessage("Zabezpečení"), + "selectALocation": + MessageLookupByLibrary.simpleMessage("Vybrat polohu"), + "selectALocationFirst": + MessageLookupByLibrary.simpleMessage("Nejprve vyberte polohu"), + "selectAlbum": MessageLookupByLibrary.simpleMessage("Vybrat album"), + "selectAll": MessageLookupByLibrary.simpleMessage("Vybrat vše"), "selectDate": MessageLookupByLibrary.simpleMessage("Vybrat datum"), + "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( + "Vyberte složky pro zálohování"), "selectLanguage": MessageLookupByLibrary.simpleMessage("Vybrat jazyk"), + "selectReason": MessageLookupByLibrary.simpleMessage("Vyberte důvod"), "selectTime": MessageLookupByLibrary.simpleMessage("Vybrat čas"), + "selectYourPlan": + MessageLookupByLibrary.simpleMessage("Vyberte svůj plán"), + "selfiesWithThem": m80, + "send": MessageLookupByLibrary.simpleMessage("Odeslat"), + "sendEmail": MessageLookupByLibrary.simpleMessage("Odeslat e-mail"), + "sendInvite": MessageLookupByLibrary.simpleMessage("Odeslat pozvánku"), "sendLink": MessageLookupByLibrary.simpleMessage("Odeslat odkaz"), + "sessionExpired": + MessageLookupByLibrary.simpleMessage("Relace vypršela"), + "setAPassword": MessageLookupByLibrary.simpleMessage("Nastavit heslo"), "setLabel": MessageLookupByLibrary.simpleMessage("Nastavit"), "setNewPassword": MessageLookupByLibrary.simpleMessage("Nastavit nové heslo"), + "setNewPin": MessageLookupByLibrary.simpleMessage("Nastavit nový PIN"), "share": MessageLookupByLibrary.simpleMessage("Sdílet"), "shareLink": MessageLookupByLibrary.simpleMessage("Sdílet odkaz"), "sharedByMe": MessageLookupByLibrary.simpleMessage("Sdíleno mnou"), "sharedByYou": MessageLookupByLibrary.simpleMessage("Sdíleno vámi"), "sharedWithMe": MessageLookupByLibrary.simpleMessage("Sdíleno se mnou"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Sdíleno s vámi"), + "sharing": MessageLookupByLibrary.simpleMessage("Sdílení..."), "skip": MessageLookupByLibrary.simpleMessage("Přeskočit"), + "sorry": MessageLookupByLibrary.simpleMessage("Omlouváme se"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Seřadit"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Seřadit podle"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Od nejnovějších"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Od nejstarších"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Zastavit přenos"), "storage": MessageLookupByLibrary.simpleMessage("Úložiště"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Rodina"), + "strongStrength": MessageLookupByLibrary.simpleMessage("Silné"), + "subscribe": MessageLookupByLibrary.simpleMessage("Odebírat"), + "subscription": MessageLookupByLibrary.simpleMessage("Předplatné"), + "suggestFeatures": + MessageLookupByLibrary.simpleMessage("Navrhnout funkce"), + "support": MessageLookupByLibrary.simpleMessage("Podpora"), + "syncStopped": + MessageLookupByLibrary.simpleMessage("Synchronizace zastavena"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronizace..."), + "systemTheme": MessageLookupByLibrary.simpleMessage("Systém"), "terminate": MessageLookupByLibrary.simpleMessage("Ukončit"), + "terminateSession": + MessageLookupByLibrary.simpleMessage("Ukončit relaci?"), + "terms": MessageLookupByLibrary.simpleMessage("Podmínky"), "thankYou": MessageLookupByLibrary.simpleMessage("Děkujeme"), + "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage( + "Stahování nebylo možné dokončit"), + "theme": MessageLookupByLibrary.simpleMessage("Motiv"), "thisDevice": MessageLookupByLibrary.simpleMessage("Toto zařízení"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("To jsem já!"), "totalSize": MessageLookupByLibrary.simpleMessage("Celková velikost"), "trash": MessageLookupByLibrary.simpleMessage("Koš"), + "tryAgain": MessageLookupByLibrary.simpleMessage("Zkusit znovu"), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "unlock": MessageLookupByLibrary.simpleMessage("Odemknout"), "unpinAlbum": MessageLookupByLibrary.simpleMessage("Odepnout album"), + "unselectAll": MessageLookupByLibrary.simpleMessage("Zrušit výběr"), + "update": MessageLookupByLibrary.simpleMessage("Aktualizovat"), + "updateAvailable": + MessageLookupByLibrary.simpleMessage("Je k dispozici aktualizace"), "upgrade": MessageLookupByLibrary.simpleMessage("Upgradovat"), + "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( + "Nahrávání souborů do alba..."), "usedSpace": MessageLookupByLibrary.simpleMessage("Využité místo"), "verify": MessageLookupByLibrary.simpleMessage("Ověřit"), + "verifyEmail": MessageLookupByLibrary.simpleMessage("Ověřit e-mail"), + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Ověřit"), "verifying": MessageLookupByLibrary.simpleMessage("Ověřování..."), + "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Ověřování obnovovacího klíče..."), "videos": MessageLookupByLibrary.simpleMessage("Videa"), "viewAll": MessageLookupByLibrary.simpleMessage("Zobrazit vše"), "viewer": MessageLookupByLibrary.simpleMessage("Prohlížející"), + "warning": MessageLookupByLibrary.simpleMessage("Varování"), + "weAreOpenSource": + MessageLookupByLibrary.simpleMessage("Jsme open source!"), + "weakStrength": MessageLookupByLibrary.simpleMessage("Slabé"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Vítejte zpět!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Co je nového"), "yearly": MessageLookupByLibrary.simpleMessage("Ročně"), @@ -258,9 +559,13 @@ class MessageLookup extends MessageLookupByLibrary { "yesDelete": MessageLookupByLibrary.simpleMessage("Ano, smazat"), "yesDiscardChanges": MessageLookupByLibrary.simpleMessage("Ano, zahodit změny"), + "yesLogout": MessageLookupByLibrary.simpleMessage("Ano, odhlásit se"), "yesRemove": MessageLookupByLibrary.simpleMessage("Ano, odstranit"), "yesRenew": MessageLookupByLibrary.simpleMessage("Ano, obnovit"), "you": MessageLookupByLibrary.simpleMessage("Vy"), + "youAndThem": m114, + "yourAccountHasBeenDeleted": + MessageLookupByLibrary.simpleMessage("Váš účet byl smazán"), "yourMap": MessageLookupByLibrary.simpleMessage("Vaše mapa") }; } diff --git a/mobile/lib/generated/intl/messages_da.dart b/mobile/lib/generated/intl/messages_da.dart index 620c3abd0c..00e5077a8c 100644 --- a/mobile/lib/generated/intl/messages_da.dart +++ b/mobile/lib/generated/intl/messages_da.dart @@ -29,24 +29,24 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "Send venligst en email til ${supportEmail} fra din registrerede email adresse"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB hver gang nogen tilmelder sig et betalt abonnement og anvender din kode"; - static String m45(expiryTime) => "Link udløber den ${expiryTime}"; + static String m46(expiryTime) => "Link udløber den ${expiryTime}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Kodeordets styrke: ${passwordStrengthValue}"; - static String m76(count) => "${count} valgt"; + static String m78(count) => "${count} valgt"; - static String m80(verificationID) => + static String m82(verificationID) => "Hey, kan du bekræfte, at dette er dit ente.io verifikation ID: ${verificationID}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m95(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; - static String m109(email) => + static String m112(email) => "Vi har sendt en email til ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -90,6 +90,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sikkerhedskopierede mapper"), "backupStatusDescription": MessageLookupByLibrary.simpleMessage( "Elementer, der er blevet sikkerhedskopieret, vil blive vist her"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( "Beklager, dette album kan ikke åbnes i appen."), "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( @@ -107,6 +108,8 @@ class MessageLookup extends MessageLookupByLibrary { "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Tjek venligst din indbakke (og spam) for at færdiggøre verificeringen"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Ryd indekser"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Kode kopieret til udklipsholder"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -219,13 +222,15 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Find folk hurtigt ved navn"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Glemt adgangskode"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Frigør enhedsplads"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Spar plads på din enhed ved at rydde filer, der allerede er sikkerhedskopieret."), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Genererer krypteringsnøgler..."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "help": MessageLookupByLibrary.simpleMessage("Hjælp"), "howItWorks": MessageLookupByLibrary.simpleMessage("Sådan fungerer det"), @@ -253,7 +258,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enheds grænse"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiveret"), "linkExpired": MessageLookupByLibrary.simpleMessage("Udløbet"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Udløb af link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Linket er udløbet"), @@ -268,6 +273,8 @@ class MessageLookup extends MessageLookupByLibrary { "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( "Langt tryk på en e-mail for at bekræfte slutningen af krypteringen."), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("Har du mistet enhed?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Maskinlæring"), @@ -289,11 +296,15 @@ class MessageLookup extends MessageLookupByLibrary { "moments": MessageLookupByLibrary.simpleMessage("Øjeblikke"), "never": MessageLookupByLibrary.simpleMessage("Aldrig"), "newAlbum": MessageLookupByLibrary.simpleMessage("Nyt album"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "next": MessageLookupByLibrary.simpleMessage("Næste"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Ingen"), "noRecoveryKey": MessageLookupByLibrary.simpleMessage("Ingen gendannelsesnøgle?"), "ok": MessageLookupByLibrary.simpleMessage("Ok"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Ups, noget gik galt"), @@ -303,7 +314,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Adgangskoden er blevet ændret"), "passwordLock": MessageLookupByLibrary.simpleMessage("Adgangskodelås"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Vi gemmer ikke denne adgangskode, så hvis du glemmer den kan vi ikke dekryptere dine data"), "pendingItems": @@ -320,6 +331,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Privatlivspolitik"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Offentligt link aktiveret"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Gendan"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Gendan konto"), "recoverButton": MessageLookupByLibrary.simpleMessage("Gendan"), @@ -375,7 +388,7 @@ class MessageLookup extends MessageLookupByLibrary { "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "Valgte mapper vil blive krypteret og sikkerhedskopieret"), - "selectedPhotos": m76, + "selectedPhotos": m78, "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendLink": MessageLookupByLibrary.simpleMessage("Send link"), "setPasswordTitle": @@ -383,7 +396,7 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Opsætning fuldført"), "shareALink": MessageLookupByLibrary.simpleMessage("Del et link"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Del med ikke Ente brugere"), "showMemories": MessageLookupByLibrary.simpleMessage("Vis minder"), @@ -402,8 +415,10 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Beklager, vi kunne ikke generere sikre krypteringsnøgler på denne enhed.\n\nForsøg venligst at oprette en konto fra en anden enhed."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "status": MessageLookupByLibrary.simpleMessage("Status"), - "storageInGB": m89, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Stærkt"), "subscribe": MessageLookupByLibrary.simpleMessage("Abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( @@ -417,7 +432,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Afslut session?"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Betingelser"), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Dette kan bruges til at gendanne din konto, hvis du mister din anden faktor"), @@ -456,7 +471,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewer": MessageLookupByLibrary.simpleMessage("Seer"), "waitingForWifi": MessageLookupByLibrary.simpleMessage("Venter på Wi-fi..."), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svagt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Velkommen tilbage!"), diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 74cae07912..ec195a96a2 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -99,225 +99,229 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} Dateien, ${formattedSize} jede"; - static String m27(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; + static String m27(name) => "Diese E-Mail ist bereits verknüpft mit ${name}."; - static String m28(email) => "${email} hat kein Ente-Konto."; + static String m28(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} hat kein Ente-Konto."; + + static String m30(email) => "${email} hat kein Ente-Konto.\n\nSende eine Einladung, um Fotos zu teilen."; - static String m30(name) => "${name} umarmen"; + static String m31(name) => "${name} umarmen"; - static String m31(text) => "Zusätzliche Fotos für ${text} gefunden"; + static String m32(text) => "Zusätzliche Fotos für ${text} gefunden"; - static String m32(name) => "Feiern mit ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; + static String m33(name) => "Feiern mit ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} in diesem Album wurde(n) sicher gespeichert"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code für einen bezahlten Tarif anmeldet"; - static String m36(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; + static String m37(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; - static String m37(count) => + static String m38(count) => "Du hast ${Intl.plural(count, one: 'darauf', other: 'auf sie')} weiterhin Zugriff, solange du ein aktives Abo hast"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; + static String m39(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Es kann vom Gerät gelöscht werden, um ${formattedSize} freizugeben', other: 'Sie können vom Gerät gelöscht werden, um ${formattedSize} freizugeben')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Verarbeite ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Wandern mit ${name}"; + static String m42(name) => "Wandern mit ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} Objekt', other: '${count} Objekte')}"; - static String m43(name) => "Zuletzt mit ${name}"; + static String m44(name) => "Zuletzt mit ${name}"; - static String m44(email) => + static String m45(email) => "${email} hat dich eingeladen, ein vertrauenswürdiger Kontakt zu werden"; - static String m45(expiryTime) => "Link läuft am ${expiryTime} ab"; + static String m46(expiryTime) => "Link läuft am ${expiryTime} ab"; - static String m46(email) => "Person mit ${email} verknüpfen"; + static String m47(email) => "Person mit ${email} verknüpfen"; - static String m47(personName, email) => + static String m48(personName, email) => "Dies wird ${personName} mit ${email} verknüpfen"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'keine Erinnerungen', one: '${formattedCount} Erinnerung', other: '${formattedCount} Erinnerungen')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Element verschieben', other: 'Elemente verschieben')}"; - static String m50(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m51(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; - static String m51(personName) => "Keine Vorschläge für ${personName}"; + static String m52(personName) => "Keine Vorschläge für ${personName}"; - static String m52(name) => "Nicht ${name}?"; + static String m53(name) => "Nicht ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Bitte wende Dich an ${familyAdminEmail}, um den Code zu ändern."; - static String m54(name) => "Party mit ${name}"; + static String m55(name) => "Party mit ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Passwortstärke: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Bitte kontaktiere den Support von ${providerName}, falls etwas abgebucht wurde"; - static String m57(name, age) => "${name} ist ${age}!"; + static String m58(name, age) => "${name} ist ${age}!"; - static String m58(name, age) => "${name} wird bald ${age}"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Keine Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; + static String m59(name, age) => "${name} wird bald ${age}"; static String m60(count) => + "${Intl.plural(count, zero: 'Keine Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 Fotos', one: 'Ein Foto', other: '${count} Fotos')}"; - static String m61(endDate) => + static String m62(endDate) => "Kostenlose Testversion gültig bis ${endDate}.\nDu kannst anschließend ein bezahltes Paket auswählen."; - static String m62(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; + static String m63(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; - static String m63(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; + static String m64(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; - static String m64(name) => "Posieren mit ${name}"; + static String m65(name) => "Posieren mit ${name}"; - static String m65(folderName) => "Verarbeite ${folderName}..."; + static String m66(folderName) => "Verarbeite ${folderName}..."; - static String m66(storeName) => "Bewerte uns auf ${storeName}"; + static String m67(storeName) => "Bewerte uns auf ${storeName}"; - static String m67(name) => "Du wurdest an ${name} neu zugewiesen"; + static String m68(name) => "Du wurdest an ${name} neu zugewiesen"; - static String m68(days, email) => + static String m69(days, email) => "Du kannst nach ${days} Tagen auf das Konto zugreifen. Eine Benachrichtigung wird an ${email} versendet."; - static String m69(email) => + static String m70(email) => "Du kannst jetzt das Konto von ${email} wiederherstellen, indem du ein neues Passwort setzt."; - static String m70(email) => + static String m71(email) => "${email} versucht, dein Konto wiederherzustellen."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ihr beide erhaltet ${storageInGB} GB* kostenlos"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefügte Fotos werden ebenfalls aus dem Album entfernt"; - static String m73(endDate) => "Erneuert am ${endDate}"; + static String m74(endDate) => "Erneuert am ${endDate}"; - static String m74(name) => "Roadtrip mit ${name}"; + static String m75(name) => "Roadtrip mit ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} Ergebnis gefunden', other: '${count} Ergebnisse gefunden')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Abschnittslänge stimmt nicht überein: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} ausgewählt"; + static String m78(count) => "${count} ausgewählt"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} ausgewählt (${yourCount} von Ihnen)"; - static String m78(name) => "Selfies mit ${name}"; + static String m80(name) => "Selfies mit ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hey, kannst du bestätigen, dass dies deine ente.io Verifizierungs-ID ist: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente Weiterempfehlungs-Code: ${referralCode} \n\nEinlösen unter Einstellungen → Allgemein → Weiterempfehlungen, um ${referralStorageInGB} GB kostenlos zu erhalten, sobald Sie einen kostenpflichtigen Tarif abgeschlossen haben\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Teile mit bestimmten Personen', one: 'Teilen mit 1 Person', other: 'Teilen mit ${numberOfPeople} Personen')}"; - static String m83(emailIDs) => "Geteilt mit ${emailIDs}"; + static String m85(emailIDs) => "Geteilt mit ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Dieses ${fileType} wird von deinem Gerät gelöscht."; - static String m85(fileType) => + static String m87(fileType) => "Diese Datei ist sowohl in Ente als auch auf deinem Gerät."; - static String m86(fileType) => "Diese Datei wird von Ente gelöscht."; + static String m88(fileType) => "Diese Datei wird von Ente gelöscht."; - static String m87(name) => "Sport mit ${name}"; + static String m89(name) => "Sport mit ${name}"; - static String m88(name) => "Spot auf ${name}"; + static String m90(name) => "Spot auf ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} von ${totalAmount} ${totalStorageUnit} verwendet"; - static String m91(id) => + static String m93(id) => "Dein ${id} ist bereits mit einem anderen Ente-Konto verknüpft.\nWenn du deine ${id} mit diesem Konto verwenden möchtest, kontaktiere bitte unseren Support"; - static String m92(endDate) => "Dein Abo endet am ${endDate}"; + static String m94(endDate) => "Dein Abo endet am ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} Erinnerungsstücke gesichert"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Zum Hochladen tippen, Hochladen wird derzeit ignoriert, da ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Diese erhalten auch ${storageAmountInGB} GB"; - static String m96(email) => "Dies ist ${email}s Verifizierungs-ID"; - - static String m97(count) => - "${Intl.plural(count, one: 'Diese Woche, vor einem Jahr', other: 'Diese Woche, vor ${count} Jahren')}"; - - static String m98(dateFormat) => "${dateFormat} über die Jahre"; + static String m98(email) => "Dies ist ${email}s Verifizierungs-ID"; static String m99(count) => + "${Intl.plural(count, one: 'Diese Woche, vor einem Jahr', other: 'Diese Woche, vor ${count} Jahren')}"; + + static String m100(dateFormat) => "${dateFormat} über die Jahre"; + + static String m101(count) => "${Intl.plural(count, zero: 'Demnächst', one: '1 Tag', other: '${count} Tage')}"; - static String m100(year) => "Reise in ${year}"; + static String m102(year) => "Reise in ${year}"; - static String m101(location) => "Ausflug nach ${location}"; + static String m103(location) => "Ausflug nach ${location}"; - static String m102(email) => + static String m104(email) => "Du wurdest von ${email} eingeladen, ein Kontakt für das digitale Erbe zu werden."; - static String m103(galleryType) => + static String m105(galleryType) => "Der Galerie-Typ ${galleryType} unterstützt kein Umbenennen"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Upload wird aufgrund von ${ignoreReason} ignoriert"; - static String m105(count) => "Sichere ${count} Erinnerungsstücke..."; + static String m107(count) => "Sichere ${count} Erinnerungsstücke..."; - static String m106(endDate) => "Gültig bis ${endDate}"; + static String m108(endDate) => "Gültig bis ${endDate}"; - static String m107(email) => "Verifiziere ${email}"; + static String m109(email) => "Verifiziere ${email}"; - static String m108(count) => + static String m110(name) => "${name} zum Entfernen des Links anzeigen"; + + static String m111(count) => "${Intl.plural(count, zero: '0 Betrachter hinzugefügt', one: 'Einen Betrachter hinzugefügt', other: '${count} Betrachter hinzugefügt')}"; - static String m109(email) => + static String m112(email) => "Wir haben eine E-Mail an ${email} gesendet"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'vor einem Jahr', other: 'vor ${count} Jahren')}"; - static String m111(name) => "Du und ${name}"; + static String m114(name) => "Du und ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Du hast ${storageSaved} erfolgreich freigegeben!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -547,26 +551,10 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("Videos sichern"), "beach": MessageLookupByLibrary.simpleMessage("Am Strand"), "birthday": MessageLookupByLibrary.simpleMessage("Geburtstag"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black-Friday-Aktion"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage( - "Massenbearbeitung von Datumsangaben"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Du kannst jetzt mehrere Fotos auswählen, und das Datum/Uhrzeit für alle mit einer Aktion ändern. Das Verschieben von Daten wird auch unterstützt."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Obergrenzen für den Familientarif"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Du kannst jetzt festlegen, wie viel Speicherplatz deine Familienmitglieder nutzen können."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Neues Icon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Endlich ein neues App-Icon, das unserer Meinung nach unser Werk am besten repräsentiert. Zudem ist es möglich, weiterhin das alte App-Icon zu verwenden."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Erinnerungen"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Entdecke Deine besonderen Momente neu – Spot auf Deine liebsten Personen, Deine Reisen und Urlaube, Deine besten Schnappschüsse und vieles mehr. Aktiviere das maschinelle Lernen, tagge Dich selbst und benenne Deine Freunde für die besten Ergebnisse."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Homescreen-Widgets mit integrierten Erinnerungen sind nun verfügbar. Sie zeigen dir deine besonderen Momente an, ohne die App zu öffnen."), "cachedData": MessageLookupByLibrary.simpleMessage("Daten im Cache"), "calculating": MessageLookupByLibrary.simpleMessage("Wird berechnet..."), @@ -638,6 +626,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Klick"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Klicken Sie auf das Überlaufmenü"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Schließen"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Nach Aufnahmezeit gruppieren"), @@ -873,6 +863,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), "editLocationTagTitle": @@ -888,16 +879,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-Mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-Mail ist bereits registriert."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-Mail nicht registriert."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-Mail-Verifizierung"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Protokolle per E-Mail senden"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Notfallkontakte"), "empty": MessageLookupByLibrary.simpleMessage("Leeren"), @@ -959,6 +950,8 @@ class MessageLookup extends MessageLookupByLibrary { "Bitte gib eine gültige E-Mail-Adresse ein."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Gib deine E-Mail-Adresse ein"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Gib Deine neue E-Mail-Adresse ein"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Passwort eingeben"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -976,7 +969,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Daten exportieren"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Zusätzliche Fotos gefunden"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gesicht ist noch nicht gruppiert, bitte komm später zurück"), "faceRecognition": @@ -1014,7 +1007,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Häufig gestellte Fragen"), "faqs": MessageLookupByLibrary.simpleMessage("FAQs"), "favorite": MessageLookupByLibrary.simpleMessage("Favorit"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Rückmeldung"), "file": MessageLookupByLibrary.simpleMessage("Datei"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1028,8 +1021,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dateitypen"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dateitypen und -namen"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dateien gelöscht"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1048,28 +1041,28 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gesichter gefunden"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Kostenlos hinzugefügter Speicherplatz"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Freier Speicherplatz nutzbar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Kostenlose Testphase"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Gerätespeicher freiräumen"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Spare Speicherplatz auf deinem Gerät, indem du Dateien löschst, die bereits gesichert wurden."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Speicherplatz freigeben"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Bis zu 1000 Erinnerungsstücke angezeigt in der Galerie"), "general": MessageLookupByLibrary.simpleMessage("Allgemein"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generierung von Verschlüsselungscodes..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Zu den Einstellungen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1083,6 +1076,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Gastansicht"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Bitte richte einen Gerätepasscode oder eine Bildschirmsperre ein, um die Gastansicht zu nutzen."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1099,7 +1094,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Geteilte Elemente in der Home-Galerie ausblenden"), "hiding": MessageLookupByLibrary.simpleMessage("Verstecken..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Gehostet bei OSM France"), "howItWorks": @@ -1158,7 +1153,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"), @@ -1179,7 +1174,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("Bitte gib diese Daten ein"), "language": MessageLookupByLibrary.simpleMessage("Sprache"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Zuletzt aktualisiert"), "lastYearsTrip": @@ -1194,7 +1189,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Digitales Erbe"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Digital geerbte Konten"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Das digitale Erbe erlaubt vertrauenswürdigen Kontakten den Zugriff auf dein Konto in deiner Abwesenheit."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1211,7 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("für schnelleres Teilen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiviert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Abgelaufen"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Ablaufdatum des Links"), "linkHasExpired": @@ -1220,13 +1215,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Person verknüpfen"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "um besseres Teilen zu ermöglichen"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live-Fotos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kannst dein Abonnement mit deiner Familie teilen"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Wir haben bereits mehr als 30 Millionen Erinnerungsstücke gesichert"), + "Wir haben bereits über 200 Millionen Erinnerungen bewahrt"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1281,6 +1276,8 @@ class MessageLookup extends MessageLookupByLibrary { "Lange auf eine E-Mail drücken, um die Ende-zu-Ende-Verschlüsselung zu überprüfen."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Drücken Sie lange auf ein Element, um es im Vollbildmodus anzuzeigen"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Videoschleife aus"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Videoschleife an"), @@ -1308,7 +1305,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ich"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage( "Mit vorhandenem zusammenführen"), @@ -1340,14 +1337,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Neuste"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Nach Relevanz"), "mountains": MessageLookupByLibrary.simpleMessage("Über den Bergen"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Ausgewählte Fotos auf ein Datum verschieben"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Zum Album verschieben"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Zu verstecktem Album verschieben"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage( "In den Papierkorb verschoben"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1362,6 +1359,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Neues Album"), "newLocation": MessageLookupByLibrary.simpleMessage("Neuer Ort"), "newPerson": MessageLookupByLibrary.simpleMessage("Neue Person"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Neue Auswahl"), "newToEnte": MessageLookupByLibrary.simpleMessage("Neu bei Ente"), "newest": MessageLookupByLibrary.simpleMessage("Zuletzt"), @@ -1401,10 +1399,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse gefunden"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Keine Systemsperre gefunden"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Nicht diese Person?"), "nothingSharedWithYouYet": @@ -1418,7 +1416,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Auf ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Wieder unterwegs"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Nur diese"), "oops": MessageLookupByLibrary.simpleMessage("Hoppla"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1448,7 +1449,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Mit PIN verbinden"), "pairingComplete": MessageLookupByLibrary.simpleMessage("Verbunden"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verifizierung steht noch aus"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1458,7 +1459,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Passwort erfolgreich geändert"), "passwordLock": MessageLookupByLibrary.simpleMessage("Passwort Sperre"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Die Berechnung der Stärke des Passworts basiert auf dessen Länge, den verwendeten Zeichen, und ob es in den 10.000 am häufigsten verwendeten Passwörtern vorkommt"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1469,7 +1470,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zahlung fehlgeschlagen"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Leider ist deine Zahlung fehlgeschlagen. Wende dich an unseren Support und wir helfen dir weiter!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Ausstehende Elemente"), "pendingSync": @@ -1483,21 +1484,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dauerhaft löschen"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Endgültig vom Gerät löschen?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Name der Person"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Pelzige Begleiter"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Foto Beschreibungen"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Fotorastergröße"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("Foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Von dir hinzugefügte Fotos werden vom Album entfernt"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Fotos behalten relativen Zeitunterschied"), @@ -1509,7 +1510,7 @@ class MessageLookup extends MessageLookupByLibrary { "Album auf dem Fernseher wiedergeben"), "playOriginal": MessageLookupByLibrary.simpleMessage("Original abspielen"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Stream abspielen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore Abo"), @@ -1522,14 +1523,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Bitte erteile die nötigen Berechtigungen"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Bitte logge dich erneut ein"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Bitte wähle die zu entfernenden schnellen Links"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bitte versuche es erneut"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1543,7 +1544,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bitte warte kurz, bevor du es erneut versuchst"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Bitte warten, dies wird eine Weile dauern."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage( "Protokolle werden vorbereitet..."), "preserveMore": @@ -1563,7 +1564,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Fortfahren"), "processed": MessageLookupByLibrary.simpleMessage("Verarbeitet"), "processing": MessageLookupByLibrary.simpleMessage("In Bearbeitung"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Verarbeite Videos"), "publicLinkCreated": @@ -1576,12 +1577,14 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Ticket erstellen"), "rateTheApp": MessageLookupByLibrary.simpleMessage("App bewerten"), "rateUs": MessageLookupByLibrary.simpleMessage("Bewerte uns"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"Ich\" neu zuweisen"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Ordne neu zu..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Wiederherstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), @@ -1591,7 +1594,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Wiederherstellung gestartet"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungs-Schlüssel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1606,12 +1609,12 @@ class MessageLookup extends MessageLookupByLibrary { "Wiederherstellungs-Schlüssel überprüft"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Dein Wiederherstellungsschlüssel ist die einzige Möglichkeit, auf deine Fotos zuzugreifen, solltest du dein Passwort vergessen. Du findest ihn unter Einstellungen > Konto.\n\nBitte gib deinen Wiederherstellungsschlüssel hier ein, um sicherzugehen, dass du ihn korrekt gesichert hast."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage( "Wiederherstellung erfolgreich!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Ein vertrauenswürdiger Kontakt versucht, auf dein Konto zuzugreifen"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Das aktuelle Gerät ist nicht leistungsfähig genug, um dein Passwort zu verifizieren, aber wir können es neu erstellen, damit es auf allen Geräten funktioniert.\n\nBitte melde dich mit deinem Wiederherstellungs-Schlüssel an und erstelle dein Passwort neu (Wenn du willst, kannst du dasselbe erneut verwenden)."), "recreatePasswordTitle": @@ -1627,7 +1630,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gib diesen Code an deine Freunde"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Sie schließen ein bezahltes Abo ab"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Weiterempfehlungen"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Einlösungen sind derzeit pausiert"), @@ -1659,7 +1662,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Link entfernen"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Personenetikett entfernen"), "removePublicLink": @@ -1679,7 +1682,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Datei umbenennen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement erneuern"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "resendEmail": @@ -1705,7 +1708,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Vorschläge überprüfen"), "right": MessageLookupByLibrary.simpleMessage("Rechts"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Drehen"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Nach links drehen"), "rotateRight": @@ -1762,8 +1765,8 @@ class MessageLookup extends MessageLookupByLibrary { "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Personen werden hier angezeigt, sobald Verarbeitung und Synchronisierung abgeschlossen sind"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Öffentliche Album-Links in der App ansehen"), @@ -1812,9 +1815,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden von dieser Person entfernt, aber nicht aus deiner Bibliothek gelöscht."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Absenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-Mail senden"), "sendInvite": MessageLookupByLibrary.simpleMessage("Einladung senden"), @@ -1844,16 +1847,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Teile jetzt ein Album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link teilen"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Teile mit ausgewählten Personen"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Hol dir Ente, damit wir ganz einfach Fotos und Videos in Originalqualität teilen können\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Mit Nicht-Ente-Benutzern teilen"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Teile dein erstes Album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1864,7 +1867,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Neue geteilte Fotos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Erhalte Benachrichtigungen, wenn jemand ein Foto zu einem gemeinsam genutzten Album hinzufügt, dem du angehörst"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Mit mir geteilt"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Mit dir geteilt"), @@ -1882,11 +1885,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Andere Geräte abmelden"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ich stimme den Nutzungsbedingungen und der Datenschutzerklärung zu"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Es wird aus allen Alben gelöscht."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Überspringen"), "social": MessageLookupByLibrary.simpleMessage("Social Media"), "someItemsAreInBothEnteAndYourDevice": @@ -1904,6 +1907,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Ein Fehler ist aufgetreten, bitte versuche es erneut"), "sorry": MessageLookupByLibrary.simpleMessage("Entschuldigung"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Leider konnten wir diese Datei momentan nicht sichern, wir werden es später erneut versuchen."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Konnte leider nicht zu den Favoriten hinzugefügt werden!"), "sorryCouldNotRemoveFromFavorites": @@ -1915,6 +1920,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Es tut uns leid, wir konnten keine sicheren Schlüssel auf diesem Gerät generieren.\n\nBitte starte die Registrierung auf einem anderen Gerät."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sortierung"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sortieren nach"), "sortNewestFirst": @@ -1923,8 +1930,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Älteste zuerst"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Abgeschlossen"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spot auf dich selbst"), "startAccountRecoveryTitle": @@ -1939,14 +1946,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Speicherplatz"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sie"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Speichergrenze überschritten"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream-Details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stark"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonnieren"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Du benötigst ein aktives, bezahltes Abonnement, um das Teilen zu aktivieren."), @@ -1964,7 +1971,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verbesserung vorschlagen"), "sunrise": MessageLookupByLibrary.simpleMessage("Am Horizont"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisierung angehalten"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronisiere …"), @@ -1977,7 +1984,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zum Entsperren antippen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Zum Hochladen antippen"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam."), "terminate": MessageLookupByLibrary.simpleMessage("Beenden"), @@ -2001,7 +2008,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Diese Elemente werden von deinem Gerät gelöscht."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Sie werden aus allen Alben gelöscht."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2019,12 +2026,12 @@ class MessageLookup extends MessageLookupByLibrary { "Dieses Bild hat keine Exif-Daten"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Das bin ich!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dies ist deine Verifizierungs-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Diese Woche über die Jahre"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dadurch wirst du von folgendem Gerät abgemeldet:"), @@ -2036,7 +2043,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Hiermit werden die öffentlichen Links aller ausgewählten schnellen Links entfernt."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Um die App-Sperre zu aktivieren, konfiguriere bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen."), @@ -2051,13 +2058,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("Gesamt"), "totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"), "trash": MessageLookupByLibrary.simpleMessage("Papierkorb"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Schneiden"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrauenswürdige Kontakte"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Erneut versuchen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Aktiviere die Sicherung, um neue Dateien in diesem Ordner automatisch zu Ente hochzuladen."), @@ -2076,7 +2083,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zwei-Faktor-Authentifizierung (2FA) erfolgreich zurückgesetzt"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) einrichten"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Dearchivieren"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album dearchivieren"), @@ -2100,10 +2107,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Ordnerauswahl wird aktualisiert..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dateien werden ins Album hochgeladen..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Sichere ein Erinnerungsstück..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2122,7 +2129,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ausgewähltes Foto verwenden"), "usedSpace": MessageLookupByLibrary.simpleMessage("Belegter Speicherplatz"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut"), @@ -2131,7 +2138,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verifizieren"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Passkey verifizieren"), @@ -2144,7 +2151,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Video-Informationen"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("Video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Video-Streaming"), + MessageLookupByLibrary.simpleMessage("Streambare Videos"), "videos": MessageLookupByLibrary.simpleMessage("Videos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktive Sitzungen anzeigen"), @@ -2157,10 +2164,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Dateien anzeigen, die den meisten Speicherplatz belegen."), "viewLogs": MessageLookupByLibrary.simpleMessage("Protokolle anzeigen"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungsschlüssel anzeigen"), "viewer": MessageLookupByLibrary.simpleMessage("Zuschauer"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bitte rufe \"web.ente.io\" auf, um dein Abo zu verwalten"), "waitingForVerification": @@ -2173,7 +2181,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Wir unterstützen keine Bearbeitung von Fotos und Alben, die du noch nicht besitzt"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Schwach"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), @@ -2182,7 +2190,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ein vertrauenswürdiger Kontakt kann helfen, deine Daten wiederherzustellen."), "yearShort": MessageLookupByLibrary.simpleMessage("Jahr"), "yearly": MessageLookupByLibrary.simpleMessage("Jährlich"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, kündigen"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2196,7 +2204,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, Person zurücksetzen"), "you": MessageLookupByLibrary.simpleMessage("Du"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Du bist im Familien-Tarif!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2215,7 +2223,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kannst nicht mit dir selbst teilen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du hast keine archivierten Elemente."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Dein Benutzerkonto wurde gelöscht"), "yourMap": MessageLookupByLibrary.simpleMessage("Deine Karte"), diff --git a/mobile/lib/generated/intl/messages_el.dart b/mobile/lib/generated/intl/messages_el.dart index 79c0433b27..679fa2404f 100644 --- a/mobile/lib/generated/intl/messages_el.dart +++ b/mobile/lib/generated/intl/messages_el.dart @@ -22,7 +22,22 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( - "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας") + "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") }; } diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 686f9835b9..3a0c25cb23 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -82,6 +82,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m21(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; + static String m116(count) => + "Also delete the photos (and videos) present in these ${count} albums from all other albums they are part of?"; + static String m22(currentlyDeleting, totalCount) => "Deleting ${currentlyDeleting} / ${totalCount}"; @@ -97,224 +100,232 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} files, ${formattedSize} each"; - static String m27(newEmail) => "Email changed to ${newEmail}"; + static String m27(name) => "This email is already linked to ${name}."; - static String m28(email) => "${email} does not have an Ente account."; + static String m28(newEmail) => "Email changed to ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} does not have an Ente account."; + + static String m30(email) => "${email} does not have an Ente account.\n\nSend them an invite to share photos."; - static String m30(name) => "Embracing ${name}"; + static String m31(name) => "Embracing ${name}"; - static String m31(text) => "Extra photos found for ${text}"; + static String m32(text) => "Extra photos found for ${text}"; - static String m32(name) => "Feasting with ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; + static String m33(name) => "Feasting with ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} in this album has been backed up safely"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB each time someone signs up for a paid plan and applies your code"; - static String m36(endDate) => "Free trial valid till ${endDate}"; + static String m37(endDate) => "Free trial valid till ${endDate}"; - static String m37(count) => + static String m38(count) => "You can still access ${Intl.plural(count, one: 'it', other: 'them')} on Ente as long as you have an active subscription"; - static String m38(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'It can be deleted from the device to free up ${formattedSize}', other: 'They can be deleted from the device to free up ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processing ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Hiking with ${name}"; + static String m117(name) => "Happy birthday to ${name}! 🎉"; - static String m42(count) => + static String m42(name) => "Hiking with ${name}"; + + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(name) => "Last time with ${name}"; + static String m44(name) => "Last time with ${name}"; - static String m44(email) => + static String m45(email) => "${email} has invited you to be a trusted contact"; - static String m45(expiryTime) => "Link will expire on ${expiryTime}"; + static String m46(expiryTime) => "Link will expire on ${expiryTime}"; - static String m46(email) => "Link person to ${email}"; + static String m47(email) => "Link person to ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "This will link ${personName} to ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'no memories', one: '${formattedCount} memory', other: '${formattedCount} memories')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Move item', other: 'Move items')}"; - static String m50(albumName) => "Moved successfully to ${albumName}"; + static String m51(albumName) => "Moved successfully to ${albumName}"; - static String m51(personName) => "No suggestions for ${personName}"; + static String m52(personName) => "No suggestions for ${personName}"; - static String m52(name) => "Not ${name}?"; + static String m53(name) => "Not ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Please contact ${familyAdminEmail} to change your code."; - static String m54(name) => "Party with ${name}"; + static String m55(name) => "Party with ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Password strength: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Please talk to ${providerName} support if you were charged"; - static String m57(name, age) => "${name} is ${age}!"; + static String m58(name, age) => "${name} is ${age}!"; - static String m58(name, age) => "${name} turning ${age} soon"; - - static String m59(count) => - "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; + static String m59(name, age) => "${name} turning ${age} soon"; static String m60(count) => + "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 photos', one: '1 photo', other: '${count} photos')}"; - static String m61(endDate) => + static String m62(endDate) => "Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards."; - static String m62(toEmail) => "Please email us at ${toEmail}"; + static String m63(toEmail) => "Please email us at ${toEmail}"; - static String m63(toEmail) => "Please send the logs to \n${toEmail}"; + static String m64(toEmail) => "Please send the logs to \n${toEmail}"; - static String m64(name) => "Posing with ${name}"; + static String m65(name) => "Posing with ${name}"; - static String m65(folderName) => "Processing ${folderName}..."; + static String m66(folderName) => "Processing ${folderName}..."; - static String m66(storeName) => "Rate us on ${storeName}"; + static String m67(storeName) => "Rate us on ${storeName}"; - static String m67(name) => "Reassigned you to ${name}"; + static String m68(name) => "Reassigned you to ${name}"; - static String m68(days, email) => + static String m69(days, email) => "You can access the account after ${days} days. A notification will be sent to ${email}."; - static String m69(email) => + static String m70(email) => "You can now recover ${email}\'s account by setting a new password."; - static String m70(email) => "${email} is trying to recover your account."; + static String m71(email) => "${email} is trying to recover your account."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Both of you get ${storageInGB} GB* free"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} will be removed from this shared album\n\nAny photos added by them will also be removed from the album"; - static String m73(endDate) => "Subscription renews on ${endDate}"; + static String m74(endDate) => "Subscription renews on ${endDate}"; - static String m74(name) => "Road trip with ${name}"; + static String m75(name) => "Road trip with ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} result found', other: '${count} results found')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Sections length mismatch: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} selected"; + static String m118(count) => "${count} selected"; - static String m77(count, yourCount) => + static String m78(count) => "${count} selected"; + + static String m79(count, yourCount) => "${count} selected (${yourCount} yours)"; - static String m78(name) => "Selfies with ${name}"; + static String m80(name) => "Selfies with ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Here\'s my verification ID: ${verificationID} for ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hey, can you confirm that this is your ente.io verification ID: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente referral code: ${referralCode} \n\nApply it in Settings → General → Referrals to get ${referralStorageInGB} GB free after you signup for a paid plan\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Share with specific people', one: 'Shared with 1 person', other: 'Shared with ${numberOfPeople} people')}"; - static String m83(emailIDs) => "Shared with ${emailIDs}"; + static String m85(emailIDs) => "Shared with ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "This ${fileType} will be deleted from your device."; - static String m85(fileType) => + static String m87(fileType) => "This ${fileType} is in both Ente and your device."; - static String m86(fileType) => "This ${fileType} will be deleted from Ente."; + static String m88(fileType) => "This ${fileType} will be deleted from Ente."; - static String m87(name) => "Sports with ${name}"; + static String m89(name) => "Sports with ${name}"; - static String m88(name) => "Spotlight on ${name}"; + static String m90(name) => "Spotlight on ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} of ${totalAmount} ${totalStorageUnit} used"; - static String m91(id) => + static String m93(id) => "Your ${id} is already linked to another Ente account.\nIf you would like to use your ${id} with this account, please contact our support\'\'"; - static String m92(endDate) => + static String m94(endDate) => "Your subscription will be cancelled on ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} memories preserved"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Tap to upload, upload is currently ignored due to ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "They also get ${storageAmountInGB} GB"; - static String m96(email) => "This is ${email}\'s Verification ID"; - - static String m97(count) => - "${Intl.plural(count, one: 'This week, ${count} year ago', other: 'This week, ${count} years ago')}"; - - static String m98(dateFormat) => "${dateFormat} through the years"; + static String m98(email) => "This is ${email}\'s Verification ID"; static String m99(count) => + "${Intl.plural(count, one: 'This week, ${count} year ago', other: 'This week, ${count} years ago')}"; + + static String m100(dateFormat) => "${dateFormat} through the years"; + + static String m101(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m100(year) => "Trip in ${year}"; + static String m102(year) => "Trip in ${year}"; - static String m101(location) => "Trip to ${location}"; + static String m103(location) => "Trip to ${location}"; - static String m102(email) => + static String m104(email) => "You have been invited to be a legacy contact by ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Type of gallery ${galleryType} is not supported for rename"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Upload is ignored due to ${ignoreReason}"; - static String m105(count) => "Preserving ${count} memories..."; + static String m107(count) => "Preserving ${count} memories..."; - static String m106(endDate) => "Valid till ${endDate}"; + static String m108(endDate) => "Valid till ${endDate}"; - static String m107(email) => "Verify ${email}"; + static String m109(email) => "Verify ${email}"; - static String m108(count) => + static String m110(name) => "View ${name} to unlink"; + + static String m111(count) => "${Intl.plural(count, zero: 'Added 0 viewers', one: 'Added 1 viewer', other: 'Added ${count} viewers')}"; - static String m109(email) => "We have sent a mail to ${email}"; + static String m112(email) => "We have sent a mail to ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} year ago', other: '${count} years ago')}"; - static String m111(name) => "You and ${name}"; + static String m114(name) => "You and ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "You have successfully freed up ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -332,11 +343,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Welcome back!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "I understand that if I lose my password, I may lose my data since my data is end-to-end encrypted."), + "actionNotSupportedOnFavouritesAlbum": + MessageLookupByLibrary.simpleMessage( + "Action not supported on Favourites album"), "activeSessions": MessageLookupByLibrary.simpleMessage("Active sessions"), "add": MessageLookupByLibrary.simpleMessage("Add"), "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Add a new email"), + "addAlbumWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add an album widget to your homescreen and come back here to customize."), "addCollaborator": MessageLookupByLibrary.simpleMessage("Add collaborator"), "addCollaborators": m1, @@ -346,6 +362,8 @@ class MessageLookup extends MessageLookupByLibrary { "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Add location"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Add"), + "addMemoriesWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add a memories widget to your homescreen and come back here to customize."), "addMore": MessageLookupByLibrary.simpleMessage("Add more"), "addName": MessageLookupByLibrary.simpleMessage("Add name"), "addNameOrMerge": @@ -356,6 +374,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Details of add-ons"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), + "addParticipants": + MessageLookupByLibrary.simpleMessage("Add participants"), + "addPeopleWidgetPrompt": MessageLookupByLibrary.simpleMessage( + "Add a people widget to your homescreen and come back here to customize."), "addPhotos": MessageLookupByLibrary.simpleMessage("Add photos"), "addSelected": MessageLookupByLibrary.simpleMessage("Add selected"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Add to album"), @@ -386,6 +408,8 @@ class MessageLookup extends MessageLookupByLibrary { "albumTitle": MessageLookupByLibrary.simpleMessage("Album title"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album updated"), "albums": MessageLookupByLibrary.simpleMessage("Albums"), + "albumsWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the albums you wish to see on your homescreen."), "allClear": MessageLookupByLibrary.simpleMessage("✨ All clear"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("All memories preserved"), @@ -530,25 +554,12 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("Backup videos"), "beach": MessageLookupByLibrary.simpleMessage("Sand and sea"), "birthday": MessageLookupByLibrary.simpleMessage("Birthday"), + "birthdayNotifications": + MessageLookupByLibrary.simpleMessage("Birthday notifications"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday Sale"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("Bulk Edit dates"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Family Plan Limits"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "You can now set limits on how much storage your family members can use."), - "cLIcon": MessageLookupByLibrary.simpleMessage("New Icon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finally, a new app icon, that we think best represents our work. We\'ve also added an icon-switcher so you can continue using the old icon."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memories"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app."), "cachedData": MessageLookupByLibrary.simpleMessage("Cached data"), "calculating": MessageLookupByLibrary.simpleMessage("Calculating..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -617,6 +628,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Click"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Click on the overflow menu"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Close"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Club by capture time"), @@ -762,6 +775,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteItemCount": m21, "deleteLocation": MessageLookupByLibrary.simpleMessage("Delete location"), + "deleteMultipleAlbumDialog": m116, "deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( @@ -848,6 +862,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Edit"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Edit location"), @@ -861,16 +876,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email already registered."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email not registered."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Email verification"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Email your logs"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Emergency Contacts"), "empty": MessageLookupByLibrary.simpleMessage("Empty"), @@ -930,6 +945,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please enter a valid email address."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Enter your email address"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Enter your new email address"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Enter your password"), "enterYourRecoveryKey": @@ -945,7 +962,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export your data"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra photos found"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Face not clustered yet, please come back later"), "faceRecognition": @@ -982,7 +999,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("FAQ"), "faqs": MessageLookupByLibrary.simpleMessage("FAQs"), "favorite": MessageLookupByLibrary.simpleMessage("Favorite"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("File"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -996,8 +1013,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("File types and names"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Files saved to gallery"), @@ -1014,26 +1031,26 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Free storage claimed"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Free storage usable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Free trial"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Free up device space"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Save space on your device by clearing files that have been already backed up."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Free up space"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Gallery"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Up to 1000 memories shown in gallery"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generating encryption keys..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Go to settings"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1046,6 +1063,9 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Guest view"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "To enable guest view, please setup device passcode or screen lock in your system settings."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "happyBirthdayToPerson": m117, "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "We don\'t track app installs. It\'d help if you told us where you found us!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1061,7 +1081,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Hide shared items from home gallery"), "hiding": MessageLookupByLibrary.simpleMessage("Hiding..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hosted at OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("How it works"), @@ -1116,7 +1136,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Items show the number of days remaining before permanent deletion"), @@ -1136,7 +1156,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Kindly help us with this information"), "language": MessageLookupByLibrary.simpleMessage("Language"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Last updated"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Last year\'s trip"), @@ -1149,7 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy allows trusted contacts to access your account in your absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1165,7 +1185,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("for faster sharing"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Enabled"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expired"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expiry"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link has expired"), @@ -1173,13 +1193,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Link person"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "for better sharing experience"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "You can share your subscription with your family"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "We have preserved over 30 million memories so far"), + "We have preserved over 200 million memories so far"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "We keep 3 copies of your data, one in an underground fallout shelter"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1234,6 +1254,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Long-press on an item to view in full-screen"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Loop video off"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Loop video on"), "lostDevice": MessageLookupByLibrary.simpleMessage("Lost device?"), @@ -1259,7 +1281,10 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Me"), - "memoryCount": m48, + "memories": MessageLookupByLibrary.simpleMessage("Memories"), + "memoriesWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the kind of memories you wish to see on your homescreen."), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Merge with existing"), @@ -1290,13 +1315,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Most recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Most relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over the hills"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Move selected photos to one date"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Move to album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Moved to trash"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Moving files to album..."), @@ -1310,6 +1335,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("New album"), "newLocation": MessageLookupByLibrary.simpleMessage("New location"), "newPerson": MessageLookupByLibrary.simpleMessage("New person"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("New range"), "newToEnte": MessageLookupByLibrary.simpleMessage("New to Ente"), "newest": MessageLookupByLibrary.simpleMessage("Newest"), @@ -1347,10 +1373,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("No results"), "noResultsFound": MessageLookupByLibrary.simpleMessage("No results found"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("No system lock found"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Not this person?"), "nothingSharedWithYouYet": @@ -1363,7 +1389,12 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "On ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("On the road again"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayMemories": + MessageLookupByLibrary.simpleMessage("On this day memories"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Only them"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": @@ -1392,7 +1423,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Pairing complete"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verification is still pending"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1402,43 +1433,47 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Password changed successfully"), "passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Password strength is calculated considering the length of the password, used characters, and whether or not the password appears in the top 10,000 most used passwords"), "passwordWarning": MessageLookupByLibrary.simpleMessage( "We don\'t store this password, so if you forget, we cannot decrypt your data"), + "pastYearsMemories": + MessageLookupByLibrary.simpleMessage("Past years\' memories"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Payment details"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Payment failed"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Unfortunately your payment failed. Please contact support and we\'ll help you out!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"), "pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"), "people": MessageLookupByLibrary.simpleMessage("People"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("People using your code"), + "peopleWidgetDesc": MessageLookupByLibrary.simpleMessage( + "Select the people you wish to see on your homescreen."), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( "All items in trash will be permanently deleted\n\nThis action cannot be undone"), "permanentlyDelete": MessageLookupByLibrary.simpleMessage("Permanently delete"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Permanently delete from device?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Person name"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Furry companions"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Photo descriptions"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Photo grid size"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("photo"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Photos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Photos added by you will be removed from the album"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Photos keep relative time difference"), @@ -1448,7 +1483,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"), "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Play original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Play stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), @@ -1461,14 +1496,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Please contact support if the problem persists"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Please grant permissions"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Please login again"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Please select quick links to remove"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Please try again"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1482,7 +1517,7 @@ class MessageLookup extends MessageLookupByLibrary { "Please wait for sometime before retrying"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Please wait, this will take a while."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparing logs..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preserve more"), @@ -1501,7 +1536,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Proceed"), "processed": MessageLookupByLibrary.simpleMessage("Processed"), "processing": MessageLookupByLibrary.simpleMessage("Processing"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processing videos"), "publicLinkCreated": @@ -1514,11 +1549,13 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Raise ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Rate the app"), "rateUs": MessageLookupByLibrary.simpleMessage("Rate us"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reassign \"Me\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reassigning..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recover"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recover account"), @@ -1527,7 +1564,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recover account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recovery initiated"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Recovery key"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Recovery key copied to clipboard"), @@ -1541,12 +1578,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recovery key verified"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Your recovery key is the only way to recover your photos if you forget your password. You can find your recovery key in Settings > Account.\n\nPlease enter your recovery key here to verify that you have saved it correctly."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recovery successful!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "A trusted contact is trying to access your account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "The current device is not powerful enough to verify your password, but we can regenerate in a way that works with all devices.\n\nPlease login using your recovery key and regenerate your password (you can use the same one again if you wish)."), "recreatePasswordTitle": @@ -1561,7 +1598,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Give this code to your friends"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. They sign up for a paid plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referrals"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referrals are currently paused"), @@ -1590,7 +1627,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remove link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remove participant"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": @@ -1610,7 +1647,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rename file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renew subscription"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Report a bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Report bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Resend email"), @@ -1635,7 +1672,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Review suggestions"), "right": MessageLookupByLibrary.simpleMessage("Right"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Rotate"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Rotate left"), "rotateRight": MessageLookupByLibrary.simpleMessage("Rotate right"), @@ -1689,8 +1726,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invite people, and you\'ll see all photos shared by them here"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "People will be shown here once processing and syncing is complete"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Security"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "See public album links in app"), @@ -1728,6 +1765,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Select your face"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Select your plan"), + "selectedAlbums": m118, "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Selected files are not on Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": @@ -1739,9 +1777,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Selected items will be removed from this person, but not deleted from your library."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"), @@ -1770,16 +1808,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Share an album now"), "shareLink": MessageLookupByLibrary.simpleMessage("Share link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Share only with the people you want"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente so we can easily share original quality photos and videos\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Share with non-Ente users"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Share your first album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1790,7 +1828,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("New shared photos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receive notifications when someone adds a photo to a shared album that you\'re a part of"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Shared with you"), @@ -1807,12 +1845,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sign out other devices"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "I agree to the terms of service and privacy policy"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "It will be deleted from all albums."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Skip"), + "smartMemories": MessageLookupByLibrary.simpleMessage("Smart memories"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1829,6 +1868,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Something went wrong, please try again"), "sorry": MessageLookupByLibrary.simpleMessage("Sorry"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Sorry, we could not backup this file right now, we will retry later."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Sorry, could not add to favorites!"), "sorryCouldNotRemoveFromFavorites": @@ -1840,13 +1881,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sort"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sort by"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Newest first"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Oldest first"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spotlight on yourself"), "startAccountRecoveryTitle": @@ -1860,14 +1903,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Storage limit exceeded"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Strong"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "You need an active paid subscription to enable sharing."), @@ -1885,7 +1928,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Suggest features"), "sunrise": MessageLookupByLibrary.simpleMessage("On the horizon"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sync stopped"), "syncing": MessageLookupByLibrary.simpleMessage("Syncing..."), "systemTheme": MessageLookupByLibrary.simpleMessage("System"), @@ -1894,7 +1937,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tap to enter code"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tap to upload"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team."), "terminate": MessageLookupByLibrary.simpleMessage("Terminate"), @@ -1917,7 +1960,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "These items will be deleted from your device."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "They will be deleted from all albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1935,12 +1978,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("This image has no exif data"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("This is me!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "This is your Verification ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("This week through the years"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "This will log you out of the following device:"), @@ -1952,7 +1995,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "This will remove public links of all selected quick links."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "To enable app lock, please setup device passcode or screen lock in your system settings."), @@ -1966,13 +2009,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Total size"), "trash": MessageLookupByLibrary.simpleMessage("Trash"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Trim"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Trusted contacts"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Try again"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Turn on backup to automatically upload files added to this device folder to Ente."), @@ -1990,7 +2033,7 @@ class MessageLookup extends MessageLookupByLibrary { "Two-factor authentication successfully reset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Two-factor setup"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Unarchive"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Unarchive album"), @@ -2013,10 +2056,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Updating folder selection..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Uploading files to album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preserving 1 memory..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2034,7 +2077,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Use selected photo"), "usedSpace": MessageLookupByLibrary.simpleMessage("Used space"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verification failed, please try again"), @@ -2042,7 +2085,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verification ID"), "verify": MessageLookupByLibrary.simpleMessage("Verify"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": @@ -2053,7 +2096,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Video Info"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Video streaming"), + MessageLookupByLibrary.simpleMessage("Streamable videos"), "videos": MessageLookupByLibrary.simpleMessage("Videos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("View active sessions"), @@ -2065,10 +2108,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "View files that are consuming the most amount of storage."), "viewLogs": MessageLookupByLibrary.simpleMessage("View logs"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("View recovery key"), "viewer": MessageLookupByLibrary.simpleMessage("Viewer"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Please visit web.ente.io to manage your subscription"), "waitingForVerification": @@ -2081,15 +2125,16 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We don\'t support editing photos and albums that you don\'t own yet"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Weak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welcome back!"), "whatsNew": MessageLookupByLibrary.simpleMessage("What\'s new"), "whyAddTrustContact": MessageLookupByLibrary.simpleMessage( "Trusted contact can help in recovering your data."), + "widgets": MessageLookupByLibrary.simpleMessage("Widgets"), "yearShort": MessageLookupByLibrary.simpleMessage("yr"), "yearly": MessageLookupByLibrary.simpleMessage("Yearly"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "yesCancel": MessageLookupByLibrary.simpleMessage("Yes, cancel"), "yesConvertToViewer": @@ -2103,7 +2148,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Yes, reset person"), "you": MessageLookupByLibrary.simpleMessage("You"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("You are on a family plan!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2122,7 +2167,7 @@ class MessageLookup extends MessageLookupByLibrary { "You cannot share with yourself"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "You don\'t have any archived items."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Your account has been deleted"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index d605770e71..510d4f58b8 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -22,9 +22,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Yo)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Añadir colaborador', one: 'Añadir colaborador', other: 'Añadir colaboradores')}"; + + static String m2(count) => + "${Intl.plural(count, one: 'Añadir objeto', other: 'Añadir objetos')}"; + static String m3(storageAmount, endDate) => "Tu ${storageAmount} adicional es válido hasta ${endDate}"; + static String m4(count) => + "${Intl.plural(count, zero: 'Añadir espectador', one: 'Añadir espectador', other: 'Añadir espectadores')}"; + static String m5(emailOrName) => "Añadido por ${emailOrName}"; static String m6(albumName) => "Añadido exitosamente a ${albumName}"; @@ -91,197 +100,225 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} archivos, ${formattedSize} cada uno"; - static String m27(newEmail) => "Correo cambiado a ${newEmail}"; + static String m28(newEmail) => "Correo cambiado a ${newEmail}"; - static String m28(email) => "${email} no tiene una cuenta de Ente."; + static String m29(email) => "${email} no tiene una cuenta de Ente."; - static String m29(email) => + static String m30(email) => "${email} no tiene una cuente en Ente.\n\nEnvíale una invitación para compartir fotos."; - static String m30(name) => "Abrazando a ${name}"; + static String m31(name) => "Abrazando a ${name}"; - static String m31(text) => "Fotos adicionales encontradas para ${text}"; + static String m32(text) => "Fotos adicionales encontradas para ${text}"; - static String m32(name) => "Festejando con ${name}"; - - static String m33(count, formattedNumber) => - "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este dispositivo de forma segura"; + static String m33(name) => "Festejando con ${name}"; static String m34(count, formattedNumber) => + "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este dispositivo de forma segura"; + + static String m35(count, formattedNumber) => "Se ha realizado la copia de seguridad de ${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} de este álbum de forma segura"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguien se registra en un plan de pago y aplica tu código"; - static String m36(endDate) => "Prueba gratuita válida hasta ${endDate}"; + static String m37(endDate) => "Prueba gratuita válida hasta ${endDate}"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m38(count) => + "Aún puedes acceder ${Intl.plural(count, one: 'a él', other: 'a ellos')} en Ente mientras tengas una suscripción activa"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Se puede eliminar del dispositivo para liberar ${formattedSize}', other: 'Se pueden eliminar del dispositivo para liberar ${formattedSize}')}"; + + static String m41(currentlyProcessing, totalCount) => "Procesando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Senderismo con ${name}"; + static String m42(name) => "Senderismo con ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementos')}"; - static String m43(name) => "Última vez con ${name}"; + static String m44(name) => "Última vez con ${name}"; - static String m44(email) => + static String m45(email) => "${email} te ha invitado a ser un contacto de confianza"; - static String m45(expiryTime) => "El enlace caducará en ${expiryTime}"; + static String m46(expiryTime) => "El enlace caducará en ${expiryTime}"; - static String m46(email) => "Enlazar persona a ${email}"; + static String m47(email) => "Enlazar persona a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Esto enlazará a ${personName} a ${email}"; - static String m50(albumName) => "Movido exitosamente a ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'no hay recuerdos', one: '${formattedCount} recuerdo', other: '${formattedCount} recuerdos')}"; - static String m51(personName) => "No hay sugerencias para ${personName}"; + static String m50(count) => + "${Intl.plural(count, one: 'Mover objeto', other: 'Mover objetos')}"; - static String m52(name) => "¿No es ${name}?"; + static String m51(albumName) => "Movido exitosamente a ${albumName}"; - static String m53(familyAdminEmail) => + static String m52(personName) => "No hay sugerencias para ${personName}"; + + static String m53(name) => "¿No es ${name}?"; + + static String m54(familyAdminEmail) => "Por favor, contacta a ${familyAdminEmail} para cambiar tu código."; - static String m54(name) => "Fiesta con ${name}"; + static String m55(name) => "Fiesta con ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Seguridad de la contraseña: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Por favor, habla con el soporte de ${providerName} si se te cobró"; - static String m58(name, age) => "${name} cumpliendo ${age} pronto"; + static String m58(name, age) => "¡${name} tiene ${age} años!"; - static String m61(endDate) => + static String m59(name, age) => "${name} cumpliendo ${age} pronto"; + + static String m60(count) => + "${Intl.plural(count, zero: 'No hay fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => + "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m62(endDate) => "Prueba gratuita válida hasta ${endDate}.\nPuedes elegir un plan de pago después."; - static String m62(toEmail) => + static String m63(toEmail) => "Por favor, envíanos un correo electrónico a ${toEmail}"; - static String m63(toEmail) => "Por favor, envía los registros a ${toEmail}"; + static String m64(toEmail) => "Por favor, envía los registros a ${toEmail}"; - static String m64(name) => "Posando con ${name}"; + static String m65(name) => "Posando con ${name}"; - static String m65(folderName) => "Procesando ${folderName}..."; + static String m66(folderName) => "Procesando ${folderName}..."; - static String m66(storeName) => "Puntúanos en ${storeName}"; + static String m67(storeName) => "Puntúanos en ${storeName}"; - static String m67(name) => "Te has reasignado a ${name}"; + static String m68(name) => "Te has reasignado a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Puedes acceder a la cuenta después de ${days} días. Se enviará una notificación a ${email}."; - static String m69(email) => + static String m70(email) => "Ahora puedes recuperar la cuenta de ${email} estableciendo una nueva contraseña."; - static String m70(email) => "${email} está intentando recuperar tu cuenta."; + static String m71(email) => "${email} está intentando recuperar tu cuenta."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ambos obtienen ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} será eliminado de este álbum compartido\n\nCualquier foto añadida por ellos también será eliminada del álbum"; - static String m73(endDate) => "La suscripción se renueva el ${endDate}"; + static String m74(endDate) => "La suscripción se renueva el ${endDate}"; - static String m74(name) => "Viaje en carretera con ${name}"; + static String m75(name) => "Viaje en carretera con ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "La longitud de las secciones no coincide: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} seleccionados"; + static String m78(count) => "${count} seleccionados"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} seleccionados (${yourCount} tuyos)"; - static String m78(name) => "Selfies con ${name}"; + static String m80(name) => "Selfies con ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Aquí está mi ID de verificación: ${verificationID} para ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hola, ¿puedes confirmar que esta es tu ID de verificación ente.io: ${verificationID}?"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Código de referido de Ente: ${referralCode} \n\nAñádelo en Ajustes → General → Referidos para obtener ${referralStorageInGB} GB gratis tras comprar un plan de pago.\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartir con personas específicas', one: 'Compartido con 1 persona', other: 'Compartido con ${numberOfPeople} personas')}"; - static String m83(emailIDs) => "Compartido con ${emailIDs}"; + static String m85(emailIDs) => "Compartido con ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Este ${fileType} se eliminará de tu dispositivo."; - static String m85(fileType) => + static String m87(fileType) => "Este ${fileType} está tanto en Ente como en tu dispositivo."; - static String m86(fileType) => "Este ${fileType} será eliminado de Ente."; + static String m88(fileType) => "Este ${fileType} será eliminado de Ente."; - static String m87(name) => "Deportes con ${name}"; + static String m89(name) => "Deportes con ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m90(name) => "Enfocar a ${name}"; - static String m90( + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usados"; - static String m91(id) => + static String m93(id) => "Tu ${id} ya está vinculada a otra cuenta de Ente.\nSi deseas utilizar tu ${id} con esta cuenta, ponte en contacto con nuestro servicio de asistencia\'\'"; - static String m92(endDate) => "Tu suscripción se cancelará el ${endDate}"; + static String m94(endDate) => "Tu suscripción se cancelará el ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} recuerdos conservados"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Toca para subir, la subida se está ignorando debido a ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "También obtienen ${storageAmountInGB} GB"; - static String m96(email) => "Este es el ID de verificación de ${email}"; - - static String m98(dateFormat) => "${dateFormat} a través de los años"; + static String m98(email) => "Este es el ID de verificación de ${email}"; static String m99(count) => + "${Intl.plural(count, one: 'Esta semana, hace ${count} año', other: 'Esta semana, hace ${count} años')}"; + + static String m100(dateFormat) => "${dateFormat} a través de los años"; + + static String m101(count) => "${Intl.plural(count, zero: 'Pronto', one: '1 día', other: '${count} días')}"; - static String m100(year) => "Viaje en ${year}"; + static String m102(year) => "Viaje en ${year}"; - static String m101(location) => "Viaje a ${location}"; + static String m103(location) => "Viaje a ${location}"; - static String m102(email) => + static String m104(email) => "Has sido invitado a ser un contacto legado por ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "El tipo de galería ${galleryType} no es compatible con el renombrado"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "La subida se ignoró debido a ${ignoreReason}"; - static String m105(count) => "Preservando ${count} memorias..."; + static String m107(count) => "Preservando ${count} memorias..."; - static String m106(endDate) => "Válido hasta ${endDate}"; + static String m108(endDate) => "Válido hasta ${endDate}"; - static String m107(email) => "Verificar ${email}"; + static String m109(email) => "Verificar ${email}"; - static String m109(email) => + static String m111(count) => + "${Intl.plural(count, zero: '0 espectadores añadidos', one: '1 espectador añadido', other: '${count} espectadores añadidos')}"; + + static String m112(email) => "Hemos enviado un correo a ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'Hace ${count} año', other: 'Hace ${count} años')}"; - static String m111(name) => "Tú y ${name}"; + static String m114(name) => "Tú y ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -307,9 +344,11 @@ class MessageLookup extends MessageLookupByLibrary { "Agregar nuevo correo electrónico"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Agregar colaborador"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Añadir archivos"), "addFromDevice": MessageLookupByLibrary.simpleMessage( "Agregar desde el dispositivo"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Agregar ubicación"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Añadir"), @@ -334,6 +373,7 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage( "Añadir contacto de confianza"), "addViewer": MessageLookupByLibrary.simpleMessage("Añadir espectador"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Añade tus fotos ahora"), "addedAs": MessageLookupByLibrary.simpleMessage("Agregado como"), @@ -362,6 +402,8 @@ class MessageLookup extends MessageLookupByLibrary { "Todos los recuerdos preservados"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( "Se eliminarán todas las agrupaciones para esta persona, y se eliminarán todas sus sugerencias"), + "allWillShiftRangeBasedOnFirst": MessageLookupByLibrary.simpleMessage( + "Este es el primero en el grupo. Otras fotos seleccionadas cambiarán automáticamente basándose en esta nueva fecha"), "allow": MessageLookupByLibrary.simpleMessage("Permitir"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( "Permitir a las personas con el enlace añadir fotos al álbum compartido."), @@ -507,22 +549,10 @@ class MessageLookup extends MessageLookupByLibrary { "Copia de seguridad de vídeos"), "beach": MessageLookupByLibrary.simpleMessage("Arena y mar "), "birthday": MessageLookupByLibrary.simpleMessage("Cumpleaños"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Oferta del Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Límites de plan familiar"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Ahora puede establecer límites en cuanto al almacenamiento que los miembros de tu familia pueden utilizar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nuevo ícono"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Por fin, un nuevo icono de la aplicación, que creemos que representa mejor nuestro trabajo. También hemos añadido una opción para que puedas seguir utilizando el icono anterior."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Recuerdos"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Ya están disponibles los widgets de pantalla de inicio con tus recuerdos. Podrás ver tus momentos especiales sin abrir la aplicación."), "cachedData": MessageLookupByLibrary.simpleMessage("Datos almacenados en caché"), "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), @@ -593,6 +623,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Clic"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Haga clic en el menú desbordante"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Cerrar"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Agrupar por tiempo de captura"), @@ -693,6 +725,8 @@ class MessageLookup extends MessageLookupByLibrary { "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Actualización crítica disponible"), "crop": MessageLookupByLibrary.simpleMessage("Ajustar encuadre"), + "curatedMemories": + MessageLookupByLibrary.simpleMessage("Memorias revisadas"), "currentUsageIs": MessageLookupByLibrary.simpleMessage("El uso actual es de "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("ejecutando"), @@ -844,16 +878,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Correo electrónico"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico ya registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico no registrado."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Verificación por correo electrónico"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Envía tus registros por correo electrónico"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contactos de emergencia"), "empty": MessageLookupByLibrary.simpleMessage("Vaciar"), @@ -935,7 +969,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar tus datos"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionales encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Cara no agrupada todavía, por favor vuelve más tarde"), "faceRecognition": @@ -974,7 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Preguntas Frecuentes"), "faqs": MessageLookupByLibrary.simpleMessage("Preguntas frecuentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Sugerencias"), "file": MessageLookupByLibrary.simpleMessage("Archivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -988,8 +1022,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de archivos"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de archivo y nombres"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Archivos eliminados"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1007,24 +1041,26 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Caras encontradas"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Almacenamiento gratuito obtenido"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Almacenamiento libre disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espacio del dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Ahorra espacio en tu dispositivo limpiando archivos que tienen copia de seguridad."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espacio"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galería"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Hasta 1000 memorias mostradas en la galería"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generando claves de cifrado..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir a Ajustes"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID de Google Play"), @@ -1038,6 +1074,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Vista de invitado"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Para habilitar la vista de invitados, por favor configure el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes de su sistema."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "No rastreamos las aplicaciones instaladas. ¡Nos ayudarías si nos dijeras dónde nos encontraste!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1054,7 +1092,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar elementos compartidos de la galería de inicio"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Alojado en OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Cómo funciona"), @@ -1111,7 +1149,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Los artículos muestran el número de días restantes antes de ser borrados permanente"), @@ -1132,7 +1170,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Por favor ayúdanos con esta información"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última actualización"), "lastYearsTrip": @@ -1147,7 +1185,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Cuentas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legado permite a los contactos de confianza acceder a su cuenta en su ausencia."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1165,7 +1203,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("para compartir más rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Habilitado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Vencido"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Enlace vence"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("El enlace ha caducado"), @@ -1173,13 +1211,11 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular persona"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "para una mejor experiencia compartida"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Foto en vivo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puedes compartir tu suscripción con tu familia"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Hasta ahora hemos conservado más de 30 millones de recuerdos"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Guardamos 3 copias de tus datos, una en un refugio subterráneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1237,6 +1273,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Manten presionado un elemento para ver en pantalla completa"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Vídeo en bucle desactivado"), "loopVideoOn": @@ -1268,6 +1306,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Yo"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Mercancías"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Combinar con existente"), @@ -1299,12 +1338,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Más reciente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Más relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sobre las colinas"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover las fotos seleccionadas a una fecha"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido a la papelera"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1320,6 +1360,7 @@ class MessageLookup extends MessageLookupByLibrary { "newLocation": MessageLookupByLibrary.simpleMessage("Nueva localización"), "newPerson": MessageLookupByLibrary.simpleMessage("Nueva persona"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Nuevo rango"), "newToEnte": MessageLookupByLibrary.simpleMessage("Nuevo en Ente"), "newest": MessageLookupByLibrary.simpleMessage("Más reciente"), @@ -1358,10 +1399,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Sin resultados"), "noResultsFound": MessageLookupByLibrary.simpleMessage( "No se han encontrado resultados"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Bloqueo de sistema no encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("¿No es esta persona?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1375,7 +1416,10 @@ class MessageLookup extends MessageLookupByLibrary { "En ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("De nuevo en la carretera"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo ellos"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1406,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Emparejamiento completo"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "La verificación aún está pendiente"), "passkey": MessageLookupByLibrary.simpleMessage("Clave de acceso"), @@ -1417,7 +1461,7 @@ class MessageLookup extends MessageLookupByLibrary { "Contraseña cambiada correctamente"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueo con contraseña"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La fortaleza de la contraseña se calcula teniendo en cuenta la longitud de la contraseña, los caracteres utilizados, y si la contraseña aparece o no en el top 10.000 de contraseñas más usadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1427,7 +1471,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Pago fallido"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Lamentablemente tu pago falló. Por favor, ¡contacta con el soporte técnico y te ayudaremos!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementos pendientes"), "pendingSync": @@ -1441,19 +1485,25 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Borrar permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "¿Eliminar permanentemente del dispositivo?"), + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nombre de la persona"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Compañeros peludos"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descripciones de fotos"), "photoGridSize": MessageLookupByLibrary.simpleMessage( "Tamaño de la cuadrícula de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Las fotos añadidas por ti serán removidas del álbum"), + "photosCount": m61, + "photosKeepRelativeTimeDifference": + MessageLookupByLibrary.simpleMessage( + "Las fotos mantienen una diferencia de tiempo relativa"), "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Elegir punto central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fijar álbum"), @@ -1462,7 +1512,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproducir álbum en TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproducir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproducir transmisión"), "playstoreSubscription": @@ -1476,14 +1526,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contacta a soporte técnico si el problema persiste"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Por favor, concede permiso"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, vuelve a iniciar sesión"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Por favor, selecciona enlaces rápidos para eliminar"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inténtalo nuevamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1498,7 +1548,7 @@ class MessageLookup extends MessageLookupByLibrary { "Por favor, espera un momento antes de volver a intentarlo"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Espera. Esto tardará un poco."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparando registros..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar más"), @@ -1517,7 +1567,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Procesado"), "processing": MessageLookupByLibrary.simpleMessage("Procesando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Procesando vídeos"), "publicLinkCreated": @@ -1531,11 +1581,13 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evalúa la aplicación"), "rateUs": MessageLookupByLibrary.simpleMessage("Califícanos"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reasignar \"Yo\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reasignando..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), @@ -1544,7 +1596,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperación iniciada"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clave de recuperación"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1559,12 +1611,12 @@ class MessageLookup extends MessageLookupByLibrary { "Clave de recuperación verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Tu clave de recuperación es la única forma de recuperar tus fotos si olvidas tu contraseña. Puedes encontrar tu clave de recuperación en Ajustes > Cuenta.\n\nPor favor, introduce tu clave de recuperación aquí para verificar que la has guardado correctamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("¡Recuperación exitosa!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contacto de confianza está intentando acceder a tu cuenta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "El dispositivo actual no es lo suficientemente potente para verificar su contraseña, pero podemos regenerarla de una manera que funcione con todos los dispositivos.\n\nPor favor inicie sesión usando su clave de recuperación y regenere su contraseña (puede volver a utilizar la misma si lo desea)."), "recreatePasswordTitle": @@ -1579,7 +1631,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dale este código a tus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Se suscriben a un plan de pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referidos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Las referencias están actualmente en pausa"), @@ -1610,7 +1662,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminar enlace"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Quitar participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminar etiqueta de persona"), "removePublicLink": @@ -1630,7 +1682,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renombrar archivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar suscripción"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar un error"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar error"), "resendEmail": @@ -1656,7 +1708,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugerencias"), "right": MessageLookupByLibrary.simpleMessage("Derecha"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Girar"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Girar a la izquierda"), @@ -1714,8 +1766,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invita a gente y verás todas las fotos compartidas aquí"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Las personas se mostrarán aquí cuando se complete el procesado y la sincronización"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver enlaces del álbum público en la aplicación"), @@ -1742,6 +1794,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Seleccionar más fotos"), "selectOneDateAndTime": MessageLookupByLibrary.simpleMessage("Seleccionar fecha y hora"), + "selectOneDateAndTimeForAll": MessageLookupByLibrary.simpleMessage( + "Seleccione una fecha y hora para todas"), "selectPersonToLink": MessageLookupByLibrary.simpleMessage( "Selecciona persona a vincular"), "selectReason": @@ -1764,9 +1818,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Los elementos seleccionados se eliminarán de esta persona, pero no se eliminarán de tu biblioteca."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar correo electrónico"), @@ -1800,16 +1854,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartir un álbum ahora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartir enlace"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Comparte sólo con la gente que quieres"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarga Ente para que podamos compartir fácilmente fotos y videos en calidad original.\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartir con usuarios fuera de Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Comparte tu primer álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1821,7 +1875,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nuevas fotos compartidas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recibir notificaciones cuando alguien agrega una foto a un álbum compartido contigo"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartido conmigo"), "sharedWithYou": @@ -1840,11 +1894,11 @@ class MessageLookup extends MessageLookupByLibrary { "Cerrar la sesión de otros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Estoy de acuerdo con los términos del servicio y la política de privacidad"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Se borrará de todos los álbumes."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Omitir"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1873,6 +1927,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Lo sentimos, no hemos podido generar claves seguras en este dispositivo.\n\nPor favor, regístrate desde un dispositivo diferente."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordenar"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordenar por"), "sortNewestFirst": @@ -1880,7 +1936,10 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Más antiguos primero"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Éxito"), - "sportsWithThem": m87, + "sportsWithThem": m89, + "spotlightOnThem": m90, + "spotlightOnYourself": + MessageLookupByLibrary.simpleMessage("Enfócate a ti mismo"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("Iniciar la recuperación"), "startBackup": @@ -1893,15 +1952,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Almacenamiento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Usted"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Límite de datos excedido"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalles de la transmisión"), "strongStrength": MessageLookupByLibrary.simpleMessage("Segura"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Suscribirse"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Necesitas una suscripción activa de pago para habilitar el compartir."), @@ -1919,7 +1978,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sugerir una característica"), "sunrise": MessageLookupByLibrary.simpleMessage("Sobre el horizonte"), "support": MessageLookupByLibrary.simpleMessage("Soporte"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronización detenida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1930,7 +1989,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToUnlock": MessageLookupByLibrary.simpleMessage("Toca para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toca para subir"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte."), "terminate": MessageLookupByLibrary.simpleMessage("Terminar"), @@ -1954,7 +2013,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estos elementos se eliminarán de tu dispositivo."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Se borrarán de todos los álbumes."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1972,11 +2031,12 @@ class MessageLookup extends MessageLookupByLibrary { "Esta imagen no tiene datos exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("¡Este soy yo!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Esta es tu ID de verificación"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana a través de los años"), + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Esto cerrará la sesión del siguiente dispositivo:"), @@ -1988,7 +2048,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Esto eliminará los enlaces públicos de todos los enlaces rápidos seleccionados."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Para habilitar el bloqueo de la aplicación, por favor configura el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes del sistema."), @@ -2002,13 +2062,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamaño total"), "trash": MessageLookupByLibrary.simpleMessage("Papelera"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Ajustar duración"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contactos de confianza"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Inténtalo de nuevo"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activar la copia de seguridad para subir automáticamente archivos añadidos a la carpeta de este dispositivo a Ente."), @@ -2026,7 +2086,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticación de doble factor restablecida con éxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configuración de dos pasos"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarchivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarchivar álbum"), @@ -2051,10 +2111,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Actualizando la selección de carpeta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Mejorar"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Subiendo archivos al álbum..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memoria..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2073,7 +2133,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto seleccionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espacio usado"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificación fallida, por favor inténtalo de nuevo"), @@ -2082,7 +2142,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Verificar correo electrónico"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar clave de acceso"), @@ -2094,8 +2154,6 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Información de video"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Transmisión de vídeo"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sesiones activas"), @@ -2112,6 +2170,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver código de recuperación"), "viewer": MessageLookupByLibrary.simpleMessage("Espectador"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Por favor, visita web.ente.io para administrar tu suscripción"), "waitingForVerification": @@ -2124,7 +2183,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "No admitimos la edición de fotos y álbumes que aún no son tuyos"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Poco segura"), "welcomeBack": MessageLookupByLibrary.simpleMessage("¡Bienvenido de nuevo!"), @@ -2133,7 +2192,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contacto de confianza puede ayudar a recuperar sus datos."), "yearShort": MessageLookupByLibrary.simpleMessage("año"), "yearly": MessageLookupByLibrary.simpleMessage("Anualmente"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sí"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sí, cancelar"), "yesConvertToViewer": @@ -2147,7 +2206,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Si, eliminar persona"), "you": MessageLookupByLibrary.simpleMessage("Tu"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("¡Estás en un plan familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2166,7 +2225,7 @@ class MessageLookup extends MessageLookupByLibrary { "No puedes compartir contigo mismo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "No tienes ningún elemento archivado."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tu cuenta ha sido eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Tu mapa"), diff --git a/mobile/lib/generated/intl/messages_et.dart b/mobile/lib/generated/intl/messages_et.dart index dc3a61a6ff..80bf2d4cf3 100644 --- a/mobile/lib/generated/intl/messages_et.dart +++ b/mobile/lib/generated/intl/messages_et.dart @@ -39,6 +39,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Luba allalaadimised"), "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Rakenda"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blog": MessageLookupByLibrary.simpleMessage("Blogi"), "cancel": MessageLookupByLibrary.simpleMessage("Loobu"), "changeEmail": MessageLookupByLibrary.simpleMessage("Muuda e-posti"), @@ -49,6 +50,8 @@ class MessageLookup extends MessageLookupByLibrary { "checkStatus": MessageLookupByLibrary.simpleMessage("Kontrolli staatust"), "checking": MessageLookupByLibrary.simpleMessage("Kontrollimine..."), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "collaborator": MessageLookupByLibrary.simpleMessage("Kaastööline"), "collectPhotos": MessageLookupByLibrary.simpleMessage("Kogu fotod"), "color": MessageLookupByLibrary.simpleMessage("Värv"), @@ -119,6 +122,8 @@ class MessageLookup extends MessageLookupByLibrary { "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage( "Grupeeri lähedal olevad fotod"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "help": MessageLookupByLibrary.simpleMessage("Abiinfo"), "hidden": MessageLookupByLibrary.simpleMessage("Peidetud"), "hide": MessageLookupByLibrary.simpleMessage("Peida"), @@ -149,6 +154,8 @@ class MessageLookup extends MessageLookupByLibrary { "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Lukusta"), "logInLabel": MessageLookupByLibrary.simpleMessage("Logi sisse"), "logout": MessageLookupByLibrary.simpleMessage("Logi välja"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "manage": MessageLookupByLibrary.simpleMessage("Halda"), "manageLink": MessageLookupByLibrary.simpleMessage("Halda linki"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Halda"), @@ -162,10 +169,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Liigutatud prügikasti"), "name": MessageLookupByLibrary.simpleMessage("Nimi"), "never": MessageLookupByLibrary.simpleMessage("Mitte kunagi"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newest": MessageLookupByLibrary.simpleMessage("Uusimad"), "no": MessageLookupByLibrary.simpleMessage("Ei"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Puudub"), "ok": MessageLookupByLibrary.simpleMessage("OK"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("Oih"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Oih, midagi läks valesti"), @@ -178,6 +189,8 @@ class MessageLookup extends MessageLookupByLibrary { "privacyPolicyTitle": MessageLookupByLibrary.simpleMessage("Privaatsus"), "radius": MessageLookupByLibrary.simpleMessage("Raadius"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recoverButton": MessageLookupByLibrary.simpleMessage("Taasta"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), "remove": MessageLookupByLibrary.simpleMessage("Eemalda"), @@ -221,6 +234,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Midagi läks valesti, palun proovi uuesti"), "sorry": MessageLookupByLibrary.simpleMessage("Vabandust"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sorteeri"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Uuemad eespool"), diff --git a/mobile/lib/generated/intl/messages_eu.dart b/mobile/lib/generated/intl/messages_eu.dart new file mode 100644 index 0000000000..2382f63f5e --- /dev/null +++ b/mobile/lib/generated/intl/messages_eu.dart @@ -0,0 +1,659 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a eu locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'eu'; + + static String m8(count) => + "${Intl.plural(count, zero: 'Parte hartzailerik ez', one: 'Parte hartzaile 1', other: '${count} Parte hartzaile')}"; + + static String m13(user) => + "${user}-(e)k ezin izango du argazki gehiago gehitu album honetan \n\nBaina haiek gehitutako argazkiak kendu ahal izango dituzte"; + + static String m14(isFamilyMember, storageAmountInGb) => + "${Intl.select(isFamilyMember, { + 'true': 'Zure familiak ${storageAmountInGb} GB eskatu du dagoeneko', + 'false': 'Zuk ${storageAmountInGb} GB eskatu duzu dagoeneko', + 'other': 'Zuk ${storageAmountInGb} GB eskatu duzu dagoeneko!', + })}"; + + static String m23(albumName) => + "Honen bidez ${albumName} eskuratzeko esteka publikoa ezabatuko da."; + + static String m24(supportEmail) => + "Mesedez, bidali e-maila ${supportEmail}-era zure erregistratutako e-mail helbidetik"; + + static String m30(email) => + "${email}-(e)k ez du Ente konturik. \n\nBidali gonbidapena argazkiak partekatzeko."; + + static String m36(storageAmountInGB) => + "${storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu"; + + static String m46(expiryTime) => + "Esteka epe honetan iraungiko da: ${expiryTime}"; + + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'oroitzapenik ez', one: 'oroitzapen ${formattedCount}', other: '${formattedCount} oroitzapen')}"; + + static String m54(familyAdminEmail) => + "Mesedez, jarri harremanetan ${familyAdminEmail}-(r)ekin zure kodea aldatzeko."; + + static String m56(passwordStrengthValue) => + "Pasahitzaren indarra: ${passwordStrengthValue}"; + + static String m72(storageInGB) => + "3. Bai zuk bai haiek ${storageInGB} GB* dohainik izango duzue"; + + static String m73(userEmail) => + "${userEmail} partekatutako album honetatik ezabatuko da \n\nHaiek gehitutako argazki guztiak ere ezabatuak izango dira albumetik"; + + static String m78(count) => "${count} hautatuta"; + + static String m79(count, yourCount) => + "${count} hautatuta (${yourCount} zureak)"; + + static String m81(verificationID) => + "Hau da nire Egiaztatze IDa: ${verificationID} ente.io-rako."; + + static String m82(verificationID) => + "Ei, baieztatu ahal duzu hau dela zure ente.io Egiaztatze IDa?: ${verificationID}"; + + static String m83(referralCode, referralStorageInGB) => + "Sartu erreferentzia kodea: ${referralCode}\n\nAplikatu hemen: Ezarpenak → Orokorra→ Erreferentziak, ${referralStorageInGB} GB dohainik izateko ordainpeko plan batean \n\nhttps://ente.io"; + + static String m84(numberOfPeople) => + "${Intl.plural(numberOfPeople, zero: 'Partekatu pertsona zehatz batzuekin', one: 'Partekatu pertsona batekin', other: 'Partekatu ${numberOfPeople} pertsonarekin')}"; + + static String m86(fileType) => "${fileType} hau zure gailutik ezabatuko da."; + + static String m87(fileType) => + "${fileType} hau Ente-n eta zure gailuan dago."; + + static String m88(fileType) => "${fileType} hau Ente-tik ezabatuko da."; + + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m97(storageAmountInGB) => + "Haiek ere lortuko dute ${storageAmountInGB} GB"; + + static String m98(email) => "Hau da ${email}-(r)en Egiaztatze IDa"; + + static String m109(email) => "Egiaztatu ${email}"; + + static String m112(email) => + "Mezua bidali dugu ${email} helbidera"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "accountWelcomeBack": + MessageLookupByLibrary.simpleMessage("Ongi etorri berriro!"), + "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( + "Nire datuak puntutik puntura zifratuta daudenez, pasahitza ahaztuz gero nire datuak gal ditzakedala ulertzen dut."), + "activeSessions": MessageLookupByLibrary.simpleMessage("Saio aktiboak"), + "addANewEmail": + MessageLookupByLibrary.simpleMessage("Gehitu e-mail berria"), + "addCollaborator": + MessageLookupByLibrary.simpleMessage("Gehitu laguntzailea"), + "addMore": MessageLookupByLibrary.simpleMessage("Gehitu gehiago"), + "addViewer": MessageLookupByLibrary.simpleMessage("Gehitu ikuslea"), + "addedAs": MessageLookupByLibrary.simpleMessage("Honela gehituta:"), + "addingToFavorites": + MessageLookupByLibrary.simpleMessage("Gogokoetan gehitzen..."), + "advancedSettings": MessageLookupByLibrary.simpleMessage("Aurreratuak"), + "after1Day": MessageLookupByLibrary.simpleMessage("Egun bat barru"), + "after1Hour": MessageLookupByLibrary.simpleMessage("Ordubete barru"), + "after1Month": + MessageLookupByLibrary.simpleMessage("Hilabete bat barru"), + "after1Week": MessageLookupByLibrary.simpleMessage("Astebete barru"), + "after1Year": MessageLookupByLibrary.simpleMessage("Urtebete barru"), + "albumOwner": MessageLookupByLibrary.simpleMessage("Jabea"), + "albumParticipantsCount": m8, + "albumUpdated": + MessageLookupByLibrary.simpleMessage("Albuma eguneratuta"), + "albums": MessageLookupByLibrary.simpleMessage("Albumak"), + "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( + "Utzi esteka duen jendeari ere album partekatuan argazkiak gehitzen."), + "allowAddingPhotos": + MessageLookupByLibrary.simpleMessage("Baimendu argazkiak gehitzea"), + "allowDownloads": + MessageLookupByLibrary.simpleMessage("Baimendu jaitsierak"), + "apply": MessageLookupByLibrary.simpleMessage("Aplikatu"), + "applyCodeTitle": + MessageLookupByLibrary.simpleMessage("Aplikatu kodea"), + "archive": MessageLookupByLibrary.simpleMessage("Artxiboa"), + "askDeleteReason": MessageLookupByLibrary.simpleMessage( + "Zein da zure kontua ezabatzeko arrazoi nagusia?"), + "authToChangeEmailVerificationSetting": + MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu emailaren egiaztatzea aldatzeko"), + "authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu pantaila blokeatzeko ezarpenak aldatzeko"), + "authToChangeYourEmail": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu zure emaila aldatzeko"), + "authToChangeYourPassword": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu zure pasahitza aldatzeko"), + "authToConfigureTwofactorAuthentication": + MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu faktore biko autentifikazioa konfiguratzeko"), + "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu kontu ezabaketa hasteko"), + "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu paperontzira botatako zure fitxategiak ikusteko"), + "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu indarrean dauden zure saioak ikusteko"), + "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu zure ezkutatutako fitxategiak ikusteko"), + "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Mesedez, autentifikatu zure berreskuratze giltza ikusteko"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "canNotOpenBody": MessageLookupByLibrary.simpleMessage( + "Sentitzen dugu, album hau ezin da aplikazioan ireki."), + "canNotOpenTitle": + MessageLookupByLibrary.simpleMessage("Ezin dut album hau ireki"), + "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( + "Zure fitxategiak baino ezin duzu ezabatu"), + "cancel": MessageLookupByLibrary.simpleMessage("Utzi"), + "cannotAddMorePhotosAfterBecomingViewer": m13, + "change": MessageLookupByLibrary.simpleMessage("Aldatu"), + "changeEmail": MessageLookupByLibrary.simpleMessage("Aldatu e-maila"), + "changePasswordTitle": + MessageLookupByLibrary.simpleMessage("Aldatu pasahitza"), + "changePermissions": + MessageLookupByLibrary.simpleMessage("Baimenak aldatu nahi?"), + "changeYourReferralCode": MessageLookupByLibrary.simpleMessage( + "Aldatu zure erreferentzia kodea"), + "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( + "Mesedez, aztertu zure inbox (eta spam) karpetak egiaztatzea osotzeko"), + "claimFreeStorage": MessageLookupByLibrary.simpleMessage( + "Eskatu debaldeko biltegiratzea"), + "claimMore": MessageLookupByLibrary.simpleMessage("Eskatu gehiago!"), + "claimed": MessageLookupByLibrary.simpleMessage("Eskatuta"), + "claimedStorageSoFar": m14, + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "codeAppliedPageTitle": + MessageLookupByLibrary.simpleMessage("Kodea aplikatuta"), + "codeChangeLimitReached": MessageLookupByLibrary.simpleMessage( + "Sentitzen dugu, zure kode aldaketa muga gainditu duzu."), + "codeCopiedToClipboard": + MessageLookupByLibrary.simpleMessage("Kodea arbelean kopiatuta"), + "codeUsedByYou": + MessageLookupByLibrary.simpleMessage("Zuk erabilitako kodea"), + "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( + "Sortu esteka bat beste pertsona batzuei zure album partekatuan arriskuak gehitu eta ikusten uzteko, naiz eta Ente aplikazio edo kontua ez izan. Oso egokia gertakizun bateko argazkiak biltzeko."), + "collaborativeLink": + MessageLookupByLibrary.simpleMessage("Parte hartzeko esteka"), + "collaborator": MessageLookupByLibrary.simpleMessage("Laguntzailea"), + "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": + MessageLookupByLibrary.simpleMessage( + "Laguntzaileek argazkiak eta bideoak gehitu ahal dituzte album partekatuan."), + "collectPhotos": + MessageLookupByLibrary.simpleMessage("Bildu argazkiak"), + "confirm": MessageLookupByLibrary.simpleMessage("Baieztatu"), + "confirm2FADisable": MessageLookupByLibrary.simpleMessage( + "Seguru zaude faktore biko autentifikazioa deuseztatu nahi duzula?"), + "confirmAccountDeletion": + MessageLookupByLibrary.simpleMessage("Baieztatu Kontu Ezabaketa"), + "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( + "Bai, betiko ezabatu nahi dut kontu hau eta berarekiko data aplikazio guztietan zehar."), + "confirmPassword": + MessageLookupByLibrary.simpleMessage("Egiaztatu pasahitza"), + "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Egiaztatu berreskuratze kodea"), + "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Egiaztatu zure berreskuratze giltza"), + "contactSupport": + MessageLookupByLibrary.simpleMessage("Kontaktatu laguntza"), + "continueLabel": MessageLookupByLibrary.simpleMessage("Jarraitu"), + "copyLink": MessageLookupByLibrary.simpleMessage("Kopiatu esteka"), + "copypasteThisCodentoYourAuthenticatorApp": + MessageLookupByLibrary.simpleMessage( + "Kopiatu eta itsatsi kode hau zure autentifikazio aplikaziora"), + "createAccount": MessageLookupByLibrary.simpleMessage("Sortu kontua"), + "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( + "Luze klikatu argazkiak hautatzeko eta klikatu + albuma sortzeko"), + "createNewAccount": + MessageLookupByLibrary.simpleMessage("Sortu kontu berria"), + "createPublicLink": + MessageLookupByLibrary.simpleMessage("Sortu esteka publikoa"), + "creatingLink": + MessageLookupByLibrary.simpleMessage("Esteka sortzen..."), + "custom": MessageLookupByLibrary.simpleMessage("Aukeran"), + "decrypting": MessageLookupByLibrary.simpleMessage("Deszifratzen..."), + "deleteAccount": + MessageLookupByLibrary.simpleMessage("Ezabatu zure kontua"), + "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( + "Sentitzen dugu zu joateaz. Mesedez, utziguzu zure feedbacka hobetzen laguntzeko."), + "deleteAccountPermanentlyButton": + MessageLookupByLibrary.simpleMessage("Ezabatu Kontua Betiko"), + "deleteAlbum": MessageLookupByLibrary.simpleMessage("Ezabatu albuma"), + "deleteAlbumDialog": MessageLookupByLibrary.simpleMessage( + "Ezabatu nahi dituzu album honetan dauden argazkiak (eta bideoak) parte diren beste album guztietatik ere?"), + "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( + "Mesedez, bidali e-mail bat account-deletion@ente.io helbidea zure erregistatutako helbidetik."), + "deleteFromBoth": + MessageLookupByLibrary.simpleMessage("Ezabatu bietatik"), + "deleteFromDevice": + MessageLookupByLibrary.simpleMessage("Ezabatu gailutik"), + "deleteFromEnte": + MessageLookupByLibrary.simpleMessage("Ezabatu Ente-tik"), + "deletePhotos": + MessageLookupByLibrary.simpleMessage("Ezabatu argazkiak"), + "deleteReason1": MessageLookupByLibrary.simpleMessage( + "Behar dudan ezaugarre nagusiren bat falta zaio"), + "deleteReason2": MessageLookupByLibrary.simpleMessage( + "Aplikazioak edo ezaugarriren batek ez du funtzionatzen nik espero nuenez"), + "deleteReason3": MessageLookupByLibrary.simpleMessage( + "Gustukoago dudan beste zerbitzu bat aurkitu dut"), + "deleteReason4": MessageLookupByLibrary.simpleMessage( + "Nire arrazoia ez dago zerrendan"), + "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( + "Zure eskaera 72 ordutan prozesatua izango da."), + "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage( + "Partekatutako albuma ezabatu?"), + "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( + "Albuma guztiontzat ezabatuko da \n\nAlbum honetan dauden beste pertsonek partekatutako argazkiak ezin izango dituzu eskuratu"), + "details": MessageLookupByLibrary.simpleMessage("Detaileak"), + "disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage( + "Ikusleek pantaila-irudiak atera ahal dituzte, edo kanpoko tresnen bidez zure argazkien kopiak gorde"), + "disableDownloadWarningTitle": + MessageLookupByLibrary.simpleMessage("Mesedez, ohartu"), + "disableLinkMessage": m23, + "discover": MessageLookupByLibrary.simpleMessage("Aurkitu"), + "discover_babies": MessageLookupByLibrary.simpleMessage("Umeak"), + "discover_celebrations": + MessageLookupByLibrary.simpleMessage("Ospakizunak"), + "discover_food": MessageLookupByLibrary.simpleMessage("Janaria"), + "discover_greenery": MessageLookupByLibrary.simpleMessage("Hostoa"), + "discover_hills": MessageLookupByLibrary.simpleMessage("Muinoak"), + "discover_identity": MessageLookupByLibrary.simpleMessage("Nortasuna"), + "discover_memes": MessageLookupByLibrary.simpleMessage("Memeak"), + "discover_notes": MessageLookupByLibrary.simpleMessage("Oharrak"), + "discover_pets": MessageLookupByLibrary.simpleMessage("Etxe-animaliak"), + "discover_receipts": + MessageLookupByLibrary.simpleMessage("Ordainagiriak"), + "discover_screenshots": + MessageLookupByLibrary.simpleMessage("Pantaila argazkiak"), + "discover_selfies": MessageLookupByLibrary.simpleMessage("Selfiak"), + "discover_sunset": + MessageLookupByLibrary.simpleMessage("Eguzki-sartzea"), + "discover_visiting_cards": + MessageLookupByLibrary.simpleMessage("Bisita txartelak"), + "discover_wallpapers": + MessageLookupByLibrary.simpleMessage("Horma-paperak"), + "doThisLater": MessageLookupByLibrary.simpleMessage("Egin hau geroago"), + "done": MessageLookupByLibrary.simpleMessage("Eginda"), + "dropSupportEmail": m24, + "eligible": MessageLookupByLibrary.simpleMessage("aukerakoak"), + "email": MessageLookupByLibrary.simpleMessage("E-maila"), + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "Helbide hau badago erregistratuta lehendik."), + "emailNoEnteAccount": m30, + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "Helbide hau ez dago erregistratuta."), + "encryption": MessageLookupByLibrary.simpleMessage("Zifratzea"), + "encryptionKeys": + MessageLookupByLibrary.simpleMessage("Zifratze giltzak"), + "entePhotosPerm": MessageLookupByLibrary.simpleMessage( + "Ente-k zure baimena behar du zure argazkiak gordetzeko"), + "enterCode": MessageLookupByLibrary.simpleMessage("Sartu kodea"), + "enterCodeDescription": MessageLookupByLibrary.simpleMessage( + "Sartu zure lagunak emandako kodea, biontzat debaldeko biltegiratzea lortzeko"), + "enterEmail": MessageLookupByLibrary.simpleMessage("Sartu e-maila"), + "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( + "Sartu pasahitz berri bat, zure data zifratu ahal izateko"), + "enterPassword": + MessageLookupByLibrary.simpleMessage("Sartu pasahitza"), + "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( + "Sartu pasahitz bat, zure data deszifratu ahal izateko"), + "enterReferralCode": + MessageLookupByLibrary.simpleMessage("Sartu erreferentzia kodea"), + "enterThe6digitCodeFromnyourAuthenticatorApp": + MessageLookupByLibrary.simpleMessage( + "Sartu 6 digituko kodea zure autentifikazio aplikaziotik"), + "enterValidEmail": MessageLookupByLibrary.simpleMessage( + "Mesedez, sartu zuzena den helbidea."), + "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( + "Sartu zure helbide elektronikoa"), + "enterYourPassword": + MessageLookupByLibrary.simpleMessage("Sartu zure pasahitza"), + "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Sartu zure berreskuratze giltza"), + "expiredLinkInfo": MessageLookupByLibrary.simpleMessage( + "Esteka hau iraungi da. Mesedez, aukeratu beste epemuga bat edo deuseztatu estekaren epemuga."), + "failedToApplyCode": + MessageLookupByLibrary.simpleMessage("Akatsa kodea aplikatzean"), + "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( + "Ezin dugu zure erreferentziaren detailerik lortu. Mesedez, saiatu berriro geroago."), + "failedToLoadAlbums": + MessageLookupByLibrary.simpleMessage("Errorea albumak kargatzen"), + "faq": MessageLookupByLibrary.simpleMessage("FAQ"), + "feedback": MessageLookupByLibrary.simpleMessage("Feedbacka"), + "forgotPassword": + MessageLookupByLibrary.simpleMessage("Ahaztu pasahitza"), + "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( + "Debaldeko biltegiratzea eskatuta"), + "freeStorageOnReferralSuccess": m36, + "freeStorageUsable": MessageLookupByLibrary.simpleMessage( + "Debaldeko biltegiratzea erabilgarri"), + "generatingEncryptionKeys": + MessageLookupByLibrary.simpleMessage("Zifratze giltzak sortzen..."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "help": MessageLookupByLibrary.simpleMessage("Laguntza"), + "hidden": MessageLookupByLibrary.simpleMessage("Ezkutatuta"), + "howItWorks": + MessageLookupByLibrary.simpleMessage("Nola funtzionatzen duen"), + "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( + "Mesedez, eska iezaiozu ezarpenen landutako bere e-mail helbidean luze klikatzeko, eta egiaztatu gailu bietako IDak bat direla."), + "iOSLockOut": MessageLookupByLibrary.simpleMessage( + "Autentifikazio biometrikoa deuseztatuta dago. Mesedez, blokeatu eta desblokeatu zure pantaila indarrean jartzeko."), + "importing": MessageLookupByLibrary.simpleMessage("Inportatzen...."), + "incorrectPasswordTitle": + MessageLookupByLibrary.simpleMessage("Pasahitz okerra"), + "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage( + "Sartu duzun berreskuratze giltza ez da zuzena"), + "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( + "Berreskuratze giltza ez da zuzena"), + "insecureDevice": + MessageLookupByLibrary.simpleMessage("Gailua ez da segurua"), + "invalidEmailAddress": + MessageLookupByLibrary.simpleMessage("Helbide hau ez da zuzena"), + "invalidKey": MessageLookupByLibrary.simpleMessage("Kode okerra"), + "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Sartu duzun berreskuratze kodea ez da zuzena. Mesedez, ziurtatu 24 hitz duela, eta egiaztatu hitz bakoitzaren idazkera. \n\nBerreskuratze kode zaharren bat sartu baduzu, ziurtatu 64 karaktere duela, eta egiaztatu horietako bakoitza."), + "inviteToEnte": + MessageLookupByLibrary.simpleMessage("Gonbidatu Ente-ra"), + "inviteYourFriends": + MessageLookupByLibrary.simpleMessage("Gonbidatu zure lagunak"), + "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( + "Hautatutako elementuak album honetatik kenduko dira"), + "keepPhotos": MessageLookupByLibrary.simpleMessage("Gorde Argazkiak"), + "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( + "Mesedez, lagun gaitzazu informazio honekin"), + "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Gailu muga"), + "linkEnabled": MessageLookupByLibrary.simpleMessage("Indarrean"), + "linkExpired": MessageLookupByLibrary.simpleMessage("Iraungita"), + "linkExpiresOn": m46, + "linkExpiry": MessageLookupByLibrary.simpleMessage("Estekaren epemuga"), + "linkHasExpired": + MessageLookupByLibrary.simpleMessage("Esteka iraungi da"), + "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Inoiz ez"), + "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Blokeatu"), + "logInLabel": MessageLookupByLibrary.simpleMessage("Sartu"), + "loginTerms": MessageLookupByLibrary.simpleMessage( + "Sartzeko klikatuz, zerbitzu baldintzak eta pribatutasun politikak onartzen ditut"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "lostDevice": + MessageLookupByLibrary.simpleMessage("Gailua galdu duzu?"), + "machineLearning": + MessageLookupByLibrary.simpleMessage("Ikasketa automatikoa"), + "magicSearch": MessageLookupByLibrary.simpleMessage("Bilaketa magikoa"), + "manage": MessageLookupByLibrary.simpleMessage("Kudeatu"), + "manageDeviceStorage": + MessageLookupByLibrary.simpleMessage("Kudeatu gailuaren katxea"), + "manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage( + "Berrikusi eta garbitu katxe lokalaren biltegiratzea."), + "manageLink": MessageLookupByLibrary.simpleMessage("Kudeatu esteka"), + "manageParticipants": MessageLookupByLibrary.simpleMessage("Kudeatu"), + "memoryCount": m49, + "mlConsent": MessageLookupByLibrary.simpleMessage( + "Aktibatu ikasketa automatikoa"), + "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( + "Ulertzen dut, eta ikasketa automatikoa aktibatu nahi dut"), + "mlConsentDescription": MessageLookupByLibrary.simpleMessage( + "Ikasketa automatikoa aktibatuz gero, Ente-k fitxategietatik informazioa aterako du (ad. argazkien geometria), zurekin partekatutako argazkietatik ere.\n\nHau zure gailuan gertatuko da, eta sortutako informazio biometrikoa puntutik puntura zifratuta egongo da."), + "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( + "Mesedez, klikatu hemen gure pribatutasun politikan ezaugarri honi buruz detaile gehiago izateko"), + "mlConsentTitle": MessageLookupByLibrary.simpleMessage( + "Ikasketa automatikoa aktibatuko?"), + "moderateStrength": MessageLookupByLibrary.simpleMessage("Ertaina"), + "movedToTrash": MessageLookupByLibrary.simpleMessage("Zarama mugituta"), + "never": MessageLookupByLibrary.simpleMessage("Inoiz ez"), + "newAlbum": MessageLookupByLibrary.simpleMessage("Album berria"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Bat ere ez"), + "noRecoveryKey": + MessageLookupByLibrary.simpleMessage("Berreskuratze giltzarik ez?"), + "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( + "Gure puntutik-puntura zifratze protokoloa dela eta, zure data ezin da deszifratu zure pasahitza edo berreskuratze giltzarik gabe"), + "ok": MessageLookupByLibrary.simpleMessage("Ondo"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, + "oops": MessageLookupByLibrary.simpleMessage("Ai!"), + "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage( + "Oops, zerbait txarto joan da"), + "orPickAnExistingOne": + MessageLookupByLibrary.simpleMessage("Edo aukeratu lehengo bat"), + "password": MessageLookupByLibrary.simpleMessage("Pasahitza"), + "passwordChangedSuccessfully": + MessageLookupByLibrary.simpleMessage("Pasahitza zuzenki aldatuta"), + "passwordLock": + MessageLookupByLibrary.simpleMessage("Pasahitza blokeoa"), + "passwordStrength": m56, + "passwordWarning": MessageLookupByLibrary.simpleMessage( + "Ezin dugu zure pasahitza gorde, beraz, ahazten baduzu, ezin dugu zure data deszifratu"), + "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( + "Jendea zure kodea erabiltzen"), + "photoGridSize": + MessageLookupByLibrary.simpleMessage("Argazki sarearen tamaina"), + "photoSmallCase": MessageLookupByLibrary.simpleMessage("argazkia"), + "pleaseTryAgain": + MessageLookupByLibrary.simpleMessage("Saiatu berriro, mesedez"), + "pleaseWait": + MessageLookupByLibrary.simpleMessage("Mesedez, itxaron..."), + "privacyPolicyTitle": + MessageLookupByLibrary.simpleMessage("Pribatutasun Politikak"), + "publicLinkEnabled": + MessageLookupByLibrary.simpleMessage("Esteka publikoa indarrean"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "recover": MessageLookupByLibrary.simpleMessage("Berreskuratu"), + "recoverAccount": + MessageLookupByLibrary.simpleMessage("Berreskuratu kontua"), + "recoverButton": MessageLookupByLibrary.simpleMessage("Berreskuratu"), + "recoveryKey": + MessageLookupByLibrary.simpleMessage("Berreskuratze giltza"), + "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( + "Berreskuratze giltza arbelean kopiatu da"), + "recoveryKeyOnForgotPassword": MessageLookupByLibrary.simpleMessage( + "Zure pasahitza ahazten baduzu, zure datuak berreskuratzeko modu bakarra gailu honen bidez izango da."), + "recoveryKeySaveDescription": MessageLookupByLibrary.simpleMessage( + "Guk ez dugu gailu hau gordetzen; mesedez, gorde 24 hitzeko giltza hau lege seguru batean."), + "recoveryKeySuccessBody": MessageLookupByLibrary.simpleMessage( + "Primeran! Zure berreskuratze giltza zuzena da. Eskerrik asko egiaztatzeagatik.\n\nMesedez, gogoratu zure berreskuratze giltza leku seguruan gordetzea."), + "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( + "Berreskuratze giltza egiaztatuta"), + "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( + "Pasahitza ahazten baduzu, zure berreskuratze giltza argazkiak berreskuratzeko modu bakarra da. Berreskuratze giltza hemen aurkitu ahal duzu Ezarpenak > Kontua.\n\nMesedez sartu hemen zure berreskuratze giltza ondo gorde duzula egiaztatzeko."), + "recoverySuccessful": MessageLookupByLibrary.simpleMessage( + "Berreskurapen arrakastatsua!"), + "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( + "Gailu hau ez da zure pasahitza egiaztatzeko bezain indartsua, baina gailu guztietan funtzionatzen duen modu batean birsortu ahal dugu. \n\nMesedez sartu zure berreskuratze giltza erabiliz eta birsortu zure pasahitza (aurreko berbera erabili ahal duzu nahi izanez gero)."), + "recreatePasswordTitle": + MessageLookupByLibrary.simpleMessage("Berrezarri pasahitza"), + "referralStep1": MessageLookupByLibrary.simpleMessage( + "1. Eman kode hau zure lagunei"), + "referralStep2": MessageLookupByLibrary.simpleMessage( + "2. Haiek ordainpeko plan batean sinatu behar dute"), + "referralStep3": m72, + "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( + "Erreferentziak momentuz geldituta daude"), + "remove": MessageLookupByLibrary.simpleMessage("Kendu"), + "removeFromAlbum": + MessageLookupByLibrary.simpleMessage("Kendu albumetik"), + "removeFromAlbumTitle": + MessageLookupByLibrary.simpleMessage("Albumetik kendu?"), + "removeLink": MessageLookupByLibrary.simpleMessage("Ezabatu esteka"), + "removeParticipant": + MessageLookupByLibrary.simpleMessage("Kendu parte hartzailea"), + "removeParticipantBody": m73, + "removePublicLink": + MessageLookupByLibrary.simpleMessage("Ezabatu esteka publikoa"), + "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( + "Kentzen ari zaren elementu batzuk beste pertsona batzuek gehitu zituzten, beraz ezin izango dituzu eskuratu"), + "removeWithQuestionMark": + MessageLookupByLibrary.simpleMessage("Ezabatuko?"), + "removingFromFavorites": + MessageLookupByLibrary.simpleMessage("Gogokoetatik kentzen..."), + "resendEmail": + MessageLookupByLibrary.simpleMessage("Birbidali e-maila"), + "resetPasswordTitle": + MessageLookupByLibrary.simpleMessage("Berrezarri pasahitza"), + "saveKey": MessageLookupByLibrary.simpleMessage("Gorde giltza"), + "saveYourRecoveryKeyIfYouHaventAlready": + MessageLookupByLibrary.simpleMessage( + "Gorde zure berreskuratze giltza ez baduzu oraindik egin"), + "scanCode": MessageLookupByLibrary.simpleMessage("Eskaneatu kodea"), + "scanThisBarcodeWithnyourAuthenticatorApp": + MessageLookupByLibrary.simpleMessage( + "Eskaneatu barra kode hau zure autentifikazio aplikazioaz"), + "selectReason": + MessageLookupByLibrary.simpleMessage("Aukeratu arrazoia"), + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "sendEmail": MessageLookupByLibrary.simpleMessage("Bidali mezua"), + "sendInvite": + MessageLookupByLibrary.simpleMessage("Bidali gonbidapena"), + "sendLink": MessageLookupByLibrary.simpleMessage("Bidali esteka"), + "setAPassword": + MessageLookupByLibrary.simpleMessage("Ezarri pasahitza"), + "setPasswordTitle": + MessageLookupByLibrary.simpleMessage("Ezarri pasahitza"), + "setupComplete": + MessageLookupByLibrary.simpleMessage("Prestaketa burututa"), + "shareALink": MessageLookupByLibrary.simpleMessage("Partekatu esteka"), + "shareMyVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, + "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( + "Jaitsi Ente argazkiak eta bideoak jatorrizko kalitatean errez partekatu ahal izateko \n\nhttps://ente.io"), + "shareTextReferralCode": m83, + "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( + "Partekatu Ente erabiltzen ez dutenekin"), + "shareWithPeopleSectionTitle": m84, + "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( + "Sortu partekatutako eta parte hartzeko albumak beste Ente erabiltzaileekin, debaldeko planak dituztenak barne."), + "sharing": MessageLookupByLibrary.simpleMessage("Partekatzen..."), + "signUpTerms": MessageLookupByLibrary.simpleMessage( + "Zerbitzu baldintzak eta pribatutasun politikak onartzen ditut"), + "singleFileDeleteFromDevice": m86, + "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( + "Album guztietatik ezabatuko da."), + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, + "someoneSharingAlbumsWithYouShouldSeeTheSameId": + MessageLookupByLibrary.simpleMessage( + "Zurekin albumak partekatzen dituen norbaitek ID berbera ikusi beharko luke bere gailuan."), + "somethingWentWrong": + MessageLookupByLibrary.simpleMessage("Zerbait oker joan da"), + "somethingWentWrongPleaseTryAgain": + MessageLookupByLibrary.simpleMessage( + "Zerbait ez da ondo joan, mesedez, saiatu berriro"), + "sorry": MessageLookupByLibrary.simpleMessage("Barkatu"), + "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( + "Sentitzen dut, ezin izan dugu zure gogokoetan gehitu!"), + "sorryCouldNotRemoveFromFavorites": + MessageLookupByLibrary.simpleMessage( + "Sentitzen dugu, ezin izan dugu zure gogokoetatik kendu!"), + "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": + MessageLookupByLibrary.simpleMessage( + "Tamalez, ezin dugu giltza segururik sortu gailu honetan. \n\nMesedez, eman izena beste gailu batetik."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), + "storageInGB": m91, + "strongStrength": MessageLookupByLibrary.simpleMessage("Gogorra"), + "subscribe": MessageLookupByLibrary.simpleMessage("Harpidetu"), + "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( + "Ordainpeko harpidetza behar duzu partekatzea aktibatzeko."), + "tapToCopy": MessageLookupByLibrary.simpleMessage("jo kopiatzeko"), + "tapToEnterCode": + MessageLookupByLibrary.simpleMessage("Klikatu kodea sartzeko"), + "terminate": MessageLookupByLibrary.simpleMessage("Bukatu"), + "terminateSession": + MessageLookupByLibrary.simpleMessage("Saioa bukatu?"), + "termsOfServicesTitle": + MessageLookupByLibrary.simpleMessage("Baldintzak"), + "theyAlsoGetXGb": m97, + "thisCanBeUsedToRecoverYourAccountIfYou": + MessageLookupByLibrary.simpleMessage( + "Hau zure kontua berreskuratzeko erabili ahal duzu, zure bigarren faktorea ahaztuz gero"), + "thisDevice": MessageLookupByLibrary.simpleMessage("Gailu hau"), + "thisIsPersonVerificationId": m98, + "thisIsYourVerificationId": + MessageLookupByLibrary.simpleMessage("Hau da zure Egiaztatze IDa"), + "thisWillLogYouOutOfTheFollowingDevice": + MessageLookupByLibrary.simpleMessage( + "Hau egiteak hurrengo gailutik aterako zaitu:"), + "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( + "Hau egiteak gailu honetatik aterako zaitu!"), + "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( + "Zure pasahitza berrezartzeko, mesedez egiaztatu zure e-maila lehenengoz."), + "total": MessageLookupByLibrary.simpleMessage("osotara"), + "trash": MessageLookupByLibrary.simpleMessage("Zarama"), + "tryAgain": MessageLookupByLibrary.simpleMessage("Saiatu berriro"), + "twofactorAuthenticationHasBeenDisabled": + MessageLookupByLibrary.simpleMessage( + "Faktore biko autentifikazioa deuseztatua izan da"), + "twofactorAuthenticationPageTitle": + MessageLookupByLibrary.simpleMessage( + "Faktore biko autentifikatzea"), + "twofactorSetup": + MessageLookupByLibrary.simpleMessage("Faktore biko ezarpena"), + "unavailableReferralCode": MessageLookupByLibrary.simpleMessage( + "Sentitzen dugu, kode hau ezin da erabili."), + "uncategorized": + MessageLookupByLibrary.simpleMessage("Kategori gabekoa"), + "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( + "Biltegiratze erabilgarria zure oraingo planaren arabera mugatuta dago. Soberan eskatutako biltegiratzea automatikoki erabili ahal izango duzu zure plan gaurkotzen duzunean."), + "useRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Erabili berreskuratze giltza"), + "verificationId": + MessageLookupByLibrary.simpleMessage("Egiaztatze IDa"), + "verify": MessageLookupByLibrary.simpleMessage("Egiaztatu"), + "verifyEmail": + MessageLookupByLibrary.simpleMessage("Egiaztatu e-maila"), + "verifyEmailID": m109, + "verifyPassword": + MessageLookupByLibrary.simpleMessage("Egiaztatu pasahitza"), + "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Berreskuratze giltza egiaztatuz..."), + "videoSmallCase": MessageLookupByLibrary.simpleMessage("bideoa"), + "viewRecoveryKey": + MessageLookupByLibrary.simpleMessage("Ikusi berreskuratze kodea"), + "viewer": MessageLookupByLibrary.simpleMessage("Ikuslea"), + "weHaveSendEmailTo": m112, + "weakStrength": MessageLookupByLibrary.simpleMessage("Ahula"), + "welcomeBack": + MessageLookupByLibrary.simpleMessage("Ongi etorri berriro!"), + "yesConvertToViewer": + MessageLookupByLibrary.simpleMessage("Bai, egin ikusle"), + "yesDelete": MessageLookupByLibrary.simpleMessage("Bai, ezabatu"), + "yesRemove": MessageLookupByLibrary.simpleMessage("Bai, ezabatu"), + "you": MessageLookupByLibrary.simpleMessage("Zu"), + "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( + "* Gehienez zure biltegiratzea bikoiztu ahal duzu"), + "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( + "Ezin duzu zeure buruarekin partekatu"), + "yourAccountHasBeenDeleted": + MessageLookupByLibrary.simpleMessage("Zure kontua ezabatua izan da") + }; +} diff --git a/mobile/lib/generated/intl/messages_fa.dart b/mobile/lib/generated/intl/messages_fa.dart index d78ffa1923..1bde0e71f2 100644 --- a/mobile/lib/generated/intl/messages_fa.dart +++ b/mobile/lib/generated/intl/messages_fa.dart @@ -28,18 +28,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "لطفا یک ایمیل از آدرس ایمیلی که ثبت نام کردید به ${supportEmail} ارسال کنید"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "قدرت رمز عبور: ${passwordStrengthValue}"; - static String m66(storeName) => "به ما در ${storeName} امتیاز دهید"; + static String m67(storeName) => "به ما در ${storeName} امتیاز دهید"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} از ${totalAmount} ${totalStorageUnit} استفاده شده"; - static String m107(email) => "تایید ${email}"; + static String m109(email) => "تایید ${email}"; - static String m109(email) => + static String m112(email) => "ما یک ایمیل به ${email} ارسال کرده‌ایم"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -90,6 +90,7 @@ class MessageLookup extends MessageLookupByLibrary { "backedUpFolders": MessageLookupByLibrary.simpleMessage("پوشه‌های پشتیبان گیری شده"), "backup": MessageLookupByLibrary.simpleMessage("پشتیبان گیری"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blog": MessageLookupByLibrary.simpleMessage("وبلاگ"), "cancel": MessageLookupByLibrary.simpleMessage("لغو"), "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( @@ -103,6 +104,8 @@ class MessageLookup extends MessageLookupByLibrary { "لطفا صندوق ورودی (و هرزنامه) خود را برای تایید کامل بررسی کنید"), "checkStatus": MessageLookupByLibrary.simpleMessage("بررسی وضعیت"), "checking": MessageLookupByLibrary.simpleMessage("در حال بررسی..."), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "پیوندی ایجاد کنید تا به افراد اجازه دهید بدون نیاز به برنامه یا حساب کاربری Ente عکس‌ها را در آلبوم اشتراک گذاشته شده شما اضافه و مشاهده کنند. برای جمع‌آوری عکس‌های رویداد عالی است."), "collaborator": MessageLookupByLibrary.simpleMessage("همکار"), @@ -218,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary { "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( "لطفا اجازه دسترسی به تمام عکس‌ها را در تنظیمات برنامه بدهید"), "grantPermission": MessageLookupByLibrary.simpleMessage("دسترسی دادن"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "ما نصب برنامه را ردیابی نمی‌کنیم. اگر بگویید کجا ما را پیدا کردید، به ما کمک می‌کند!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -241,14 +246,14 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "لطفا با این اطلاعات به ما کمک کنید"), "lightTheme": MessageLookupByLibrary.simpleMessage("روشن"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "ما تا کنون بیش از ۳۰ میلیون خاطره را حفظ کرده‌ایم"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("قفل"), "logInLabel": MessageLookupByLibrary.simpleMessage("ورود"), "loggingOut": MessageLookupByLibrary.simpleMessage("در حال خروج..."), "loginTerms": MessageLookupByLibrary.simpleMessage( "با کلیک بر روی ورود به سیستم، من با شرایط خدمات و سیاست حفظ حریم خصوصی موافقم"), "logout": MessageLookupByLibrary.simpleMessage("خروج"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "manage": MessageLookupByLibrary.simpleMessage("مدیریت"), "manageFamily": MessageLookupByLibrary.simpleMessage("مدیریت خانواده"), "manageLink": MessageLookupByLibrary.simpleMessage("مدیریت پیوند"), @@ -260,6 +265,7 @@ class MessageLookup extends MessageLookupByLibrary { "merchandise": MessageLookupByLibrary.simpleMessage("کالا"), "moderateStrength": MessageLookupByLibrary.simpleMessage("متوسط"), "never": MessageLookupByLibrary.simpleMessage("هرگز"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("کاربر جدید Ente"), "no": MessageLookupByLibrary.simpleMessage("خیر"), "noRecoveryKey": @@ -268,11 +274,14 @@ class MessageLookup extends MessageLookupByLibrary { "با توجه به ماهیت پروتکل رمزگذاری سرتاسر ما، اطلاعات شما بدون رمز عبور یا کلید بازیابی شما قابل رمزگشایی نیست"), "notifications": MessageLookupByLibrary.simpleMessage("آگاه‌سازی‌ها"), "ok": MessageLookupByLibrary.simpleMessage("تایید"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("اوه"), "password": MessageLookupByLibrary.simpleMessage("رمز عبور"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "رمز عبور با موفقیت تغییر کرد"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "ما این رمز عبور را ذخیره نمی‌کنیم، بنابراین اگر فراموش کنید، نمی‌توانیم اطلاعات شما را رمزگشایی کنیم"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("عکس"), @@ -292,7 +301,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("پشتیبان گیری خصوصی"), "privateSharing": MessageLookupByLibrary.simpleMessage("اشتراک گذاری خصوصی"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("بازیابی"), "recoverAccount": MessageLookupByLibrary.simpleMessage("بازیابی حساب کاربری"), @@ -355,6 +366,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "با عرض پوزش، ما نمی‌توانیم کلیدهای امن را در این دستگاه تولید کنیم.\n\nلطفا از دستگاه دیگری ثبت نام کنید."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("مرتب‌سازی براساس"), "sortNewestFirst": @@ -368,7 +381,7 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("خانوادگی"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("شما"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("قوی"), "support": MessageLookupByLibrary.simpleMessage("پشتیبانی"), "systemTheme": MessageLookupByLibrary.simpleMessage("سیستم"), @@ -409,7 +422,7 @@ class MessageLookup extends MessageLookupByLibrary { "از کلید بازیابی استفاده کنید"), "verify": MessageLookupByLibrary.simpleMessage("تایید"), "verifyEmail": MessageLookupByLibrary.simpleMessage("تایید ایمیل"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("تایید"), "verifyPassword": MessageLookupByLibrary.simpleMessage("تایید رمز عبور"), @@ -422,7 +435,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewer": MessageLookupByLibrary.simpleMessage("بیننده"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("ما متن‌باز هستیم!"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("ضعیف"), "welcomeBack": MessageLookupByLibrary.simpleMessage("خوش آمدید!"), "whatsNew": MessageLookupByLibrary.simpleMessage("تغییرات جدید"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index c2fd41c969..08b4acd2d5 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -22,9 +22,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Moi)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Ajouter un collaborateur', one: 'Ajouter un collaborateur', other: 'Ajouter des collaborateurs')}"; + + static String m2(count) => + "${Intl.plural(count, one: 'Ajouter un élément', other: 'Ajouter des éléments')}"; + static String m3(storageAmount, endDate) => "Votre extension de ${storageAmount} est valable jusqu\'au ${endDate}"; + static String m4(count) => + "${Intl.plural(count, zero: 'Ajouter un spectateur', one: 'Ajouter une spectateur', other: 'Ajouter des spectateurs')}"; + static String m5(emailOrName) => "Ajouté par ${emailOrName}"; static String m6(albumName) => "Ajouté avec succès à ${albumName}"; @@ -91,206 +100,228 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} fichiers, ${formattedSize} chacun"; - static String m27(newEmail) => "L\'email a été changé par ${newEmail}"; + static String m27(name) => "Cet e-mail est déjà lié à ${name}."; - static String m28(email) => "${email} n\'a pas de compte Ente."; + static String m28(newEmail) => "L\'email a été changé par ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} n\'a pas de compte Ente."; + + static String m30(email) => "${email} n\'a pas de compte Ente.\n\nEnvoyez une invitation pour partager des photos."; - static String m30(name) => "Embrasse ${name}"; + static String m31(name) => "Embrasse ${name}"; - static String m31(text) => "Photos supplémentaires trouvées pour ${text}"; + static String m32(text) => "Photos supplémentaires trouvées pour ${text}"; - static String m32(name) => "Fête avec ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 fichier sur cet appareil a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers sur cet appareil ont été sauvegardés en toute sécurité')}"; + static String m33(name) => "Fête avec ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 fichier sur cet appareil a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers sur cet appareil ont été sauvegardés en toute sécurité')}"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 fichier dans cet album a été sauvegardé en toute sécurité', other: '${formattedNumber} fichiers dans cet album ont été sauvegardés en toute sécurité')}"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} Go chaque fois que quelqu\'un s\'inscrit à une offre payante et applique votre code"; - static String m36(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; + static String m37(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; - static String m38(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + static String m38(count) => + "Vous pouvez toujours ${Intl.plural(count, one: 'l\'', other: 'les')} accéder sur Ente tant que vous avez un abonnement actif"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Il peut être supprimé de l\'appareil pour libérer ${formattedSize}', other: 'Ils peuvent être supprimés de l\'appareil pour libérer ${formattedSize}')}"; + + static String m41(currentlyProcessing, totalCount) => "Traitement en cours ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Randonnée avec ${name}"; + static String m42(name) => "Randonnée avec ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} objet', other: '${count} objets')}"; - static String m43(name) => "Dernière fois avec ${name}"; + static String m44(name) => "Dernière fois avec ${name}"; - static String m44(email) => + static String m45(email) => "${email} vous a invité à être un contact de confiance"; - static String m45(expiryTime) => "Le lien expirera le ${expiryTime}"; + static String m46(expiryTime) => "Le lien expirera le ${expiryTime}"; - static String m46(email) => "Associer la personne à ${email}"; + static String m47(email) => "Associer la personne à ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Cela va associer ${personName} à ${email}"; - static String m50(albumName) => "Déplacé avec succès vers ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'aucun souvenir', one: '${formattedCount} souvenir', other: '${formattedCount} souvenirs')}"; - static String m51(personName) => "Aucune suggestion pour ${personName}"; + static String m50(count) => + "${Intl.plural(count, one: 'Déplacer un élément', other: 'Déplacer des éléments')}"; - static String m52(name) => "Pas ${name}?"; + static String m51(albumName) => "Déplacé avec succès vers ${albumName}"; - static String m53(familyAdminEmail) => + static String m52(personName) => "Aucune suggestion pour ${personName}"; + + static String m53(name) => "Pas ${name}?"; + + static String m54(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour modifier votre code."; - static String m54(name) => "En soirée avec ${name}"; + static String m55(name) => "En soirée avec ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Sécurité du mot de passe : ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Veuillez contacter le support ${providerName} si vous avez été facturé"; - static String m57(name, age) => "${name} a ${age}!"; + static String m58(name, age) => "${name} a ${age}!"; - static String m58(name, age) => "${name} aura bientôt ${age}"; + static String m59(name, age) => "${name} aura bientôt ${age}"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'No photos', one: '1 photo', other: '${count} photos')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 photo', one: '1 photo', other: '${count} photos')}"; + + static String m62(endDate) => "Essai gratuit valable jusqu\'à ${endDate}.\nVous pouvez choisir un plan payant par la suite."; - static String m62(toEmail) => "Merci de nous envoyer un email à ${toEmail}"; + static String m63(toEmail) => "Merci de nous envoyer un email à ${toEmail}"; - static String m63(toEmail) => "Envoyez les logs à ${toEmail}"; + static String m64(toEmail) => "Envoyez les logs à ${toEmail}"; - static String m64(name) => "Pose avec ${name}"; + static String m65(name) => "Pose avec ${name}"; - static String m65(folderName) => "Traitement de ${folderName}..."; + static String m66(folderName) => "Traitement de ${folderName}..."; - static String m66(storeName) => "Laissez une note sur ${storeName}"; + static String m67(storeName) => "Laissez une note sur ${storeName}"; - static String m67(name) => "Vous a réassigné à ${name}"; + static String m68(name) => "Vous a réassigné à ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Vous pourrez accéder au compte d\'ici ${days} jours. Une notification sera envoyée à ${email}."; - static String m69(email) => + static String m70(email) => "Vous pouvez maintenant récupérer le compte de ${email} en définissant un nouveau mot de passe."; - static String m70(email) => "${email} tente de récupérer votre compte."; + static String m71(email) => "${email} tente de récupérer votre compte."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Vous recevez tous les deux ${storageInGB} Go* gratuits"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} sera retiré de cet album partagé\n\nToutes les photos ajoutées par eux seront également retirées de l\'album"; - static String m73(endDate) => "Renouvellement le ${endDate}"; + static String m74(endDate) => "Renouvellement le ${endDate}"; - static String m74(name) => "En route avec ${name}"; + static String m75(name) => "En route avec ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilité de longueur des sections : ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} sélectionné(s)"; + static String m78(count) => "${count} sélectionné(s)"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} sélectionné(s) (${yourCount} à vous)"; - static String m78(name) => "Selfies avec ${name}"; + static String m80(name) => "Selfies avec ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Voici mon ID de vérification : ${verificationID} pour ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hé, pouvez-vous confirmer qu\'il s\'agit de votre ID de vérification ente.io : ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Code de parrainage Ente : ${referralCode} \n\nValidez le dans Paramètres → Général → Références pour obtenir ${referralStorageInGB} Go gratuitement après votre inscription à un plan payant\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partagez avec des personnes spécifiques', one: 'Partagé avec 1 personne', other: 'Partagé avec ${numberOfPeople} personnes')}"; - static String m83(emailIDs) => "Partagé avec ${emailIDs}"; + static String m85(emailIDs) => "Partagé avec ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Elle ${fileType} sera supprimée de votre appareil."; - static String m85(fileType) => + static String m87(fileType) => "Cette ${fileType} est à la fois sur ente et sur votre appareil."; - static String m86(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; + static String m88(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; - static String m87(name) => "Sports avec ${name}"; + static String m89(name) => "Sports avec ${name}"; - static String m88(name) => "Spotlight sur ${name}"; + static String m90(name) => "Spotlight sur ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} Go"; + static String m91(storageAmountInGB) => "${storageAmountInGB} Go"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} sur ${totalAmount} ${totalStorageUnit} utilisés"; - static String m91(id) => + static String m93(id) => "Votre ${id} est déjà lié à un autre compte Ente.\nSi vous souhaitez utiliser votre ${id} avec ce compte, veuillez contacter notre support"; - static String m92(endDate) => "Votre abonnement sera annulé le ${endDate}"; + static String m94(endDate) => "Votre abonnement sera annulé le ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} souvenirs sauvegardés"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Appuyer pour envoyer, l\'envoi est actuellement ignoré en raison de ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Ils obtiennent aussi ${storageAmountInGB} Go"; - static String m96(email) => "Ceci est l\'ID de vérification de ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'Cette semaine, ${count} il y a l\'année', other: 'Cette semaine, ${count} il y a des années')}"; - - static String m98(dateFormat) => "${dateFormat} au fil des années"; + static String m98(email) => "Ceci est l\'ID de vérification de ${email}"; static String m99(count) => + "${Intl.plural(count, one: 'Cette semaine, ${count} il y a l\'année', other: 'Cette semaine, ${count} il y a des années')}"; + + static String m100(dateFormat) => "${dateFormat} au fil des années"; + + static String m101(count) => "${Intl.plural(count, zero: 'Bientôt', one: '1 jour', other: '${count} jours')}"; - static String m100(year) => "Voyage en ${year}"; + static String m102(year) => "Voyage en ${year}"; - static String m101(location) => "Voyage vers ${location}"; + static String m103(location) => "Voyage vers ${location}"; - static String m102(email) => + static String m104(email) => "Vous avez été invité(e) à être un(e) héritier(e) par ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Les galeries de type \'${galleryType}\' ne peuvent être renommées"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "L\'envoi est ignoré en raison de ${ignoreReason}"; - static String m105(count) => "Sauvegarde de ${count} souvenirs..."; + static String m107(count) => "Sauvegarde de ${count} souvenirs..."; - static String m106(endDate) => "Valable jusqu\'au ${endDate}"; + static String m108(endDate) => "Valable jusqu\'au ${endDate}"; - static String m107(email) => "Vérifier ${email}"; + static String m109(email) => "Vérifier ${email}"; - static String m109(email) => + static String m110(name) => "Voir ${name} pour délier"; + + static String m111(count) => + "${Intl.plural(count, zero: '0 spectateur ajouté', one: 'Un spectateur ajouté', other: '${count} spectateurs ajoutés')}"; + + static String m112(email) => "Nous avons envoyé un email à ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'il y a ${count} an', other: 'il y a ${count} ans')}"; - static String m111(name) => "Vous et ${name}"; + static String m114(name) => "Vous et ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Vous avez libéré ${storageSaved} avec succès !"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -316,10 +347,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ajouter un nouvel email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Ajouter un collaborateur"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Ajouter des fichiers"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Ajouter depuis l\'appareil"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Ajouter la localisation"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Ajouter"), @@ -347,6 +380,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ajouter un contact de confiance"), "addViewer": MessageLookupByLibrary.simpleMessage("Ajouter un observateur"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Ajoutez vos photos maintenant"), "addedAs": MessageLookupByLibrary.simpleMessage("Ajouté comme"), @@ -522,26 +556,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sauvegarde des vidéos"), "beach": MessageLookupByLibrary.simpleMessage("Sable et mer"), "birthday": MessageLookupByLibrary.simpleMessage("Anniversaire"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Offre Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage( - "Dates de modification multiples"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites pour le forfait Famille"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Vous pouvez maintenant fixer des limites sur la quantité de stockage que les membres de votre famille peuvent utiliser."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nouvel icône"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalement, création d\'un nouvel icône d\'application qui, selon nous, représente au mieux notre travail. Nous avons également ajouté un changeur d\'icône pour que vous puissiez continuer à utiliser l\'ancien."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Souvenirs"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redécouvrez vos précieux souvenirs - focus sur vos connaissances préférées, vos voyages et vos vacances, vos meilleurs clics et bien plus encore. Activez l\'apprentissage automatique, taguez-vous et nommez vos amis pour une meilleure expérience."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Les widgets (ou gadgets) de l\'écran d\'accueil, qui sont intégrés à des souvenirs, sont maintenant disponibles. Ils montreront vos moments spéciaux sans nécessité d\'ouvrir l\'application."), "cachedData": MessageLookupByLibrary.simpleMessage("Données mises en cache"), "calculating": @@ -615,6 +633,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Cliquez sur"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Cliquez sur le menu de débordement"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Fermer"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Grouper par durée"), @@ -716,6 +736,8 @@ class MessageLookup extends MessageLookupByLibrary { "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Mise à jour critique disponible"), "crop": MessageLookupByLibrary.simpleMessage("Rogner"), + "curatedMemories": + MessageLookupByLibrary.simpleMessage("Souvenirs conservés"), "currentUsageIs": MessageLookupByLibrary.simpleMessage( "L\'utilisation actuelle est de "), "currentlyRunning": @@ -857,6 +879,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Modifier l’emplacement"), "editLocationTagTitle": @@ -873,16 +896,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email déjà enregistré."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail non enregistré."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Authentification à deux facteurs par email"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Envoyez vos journaux par email"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contacts d\'urgence"), "empty": MessageLookupByLibrary.simpleMessage("Vider"), @@ -944,6 +967,8 @@ class MessageLookup extends MessageLookupByLibrary { "Veuillez entrer une adresse email valide."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Entrez votre adresse e-mail"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Entrez votre nouvelle adresse e-mail"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Entrez votre mot de passe"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -960,7 +985,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportez vos données"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Photos supplémentaires trouvées"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Ce visage n\'a pas encore été regroupé, veuillez revenir plus tard"), "faceRecognition": @@ -999,7 +1024,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("FAQ"), "faqs": MessageLookupByLibrary.simpleMessage("FAQ"), "favorite": MessageLookupByLibrary.simpleMessage("Favori"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Commentaires"), "file": MessageLookupByLibrary.simpleMessage("Fichier"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1013,8 +1038,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Types de fichiers"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Types et noms de fichiers"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fichiers supprimés"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1032,25 +1057,27 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Visages trouvés"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Stockage gratuit obtenu"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Stockage gratuit disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Essai gratuit"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Libérer de l\'espace sur l\'appareil"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Économisez de l\'espace sur votre appareil en effaçant les fichiers qui ont déjà été sauvegardés."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libérer de l\'espace"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Jusqu\'à 1000 souvenirs affichés dans la galerie"), "general": MessageLookupByLibrary.simpleMessage("Général"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Génération des clés de chiffrement..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Allez aux réglages"), "googlePlayId": @@ -1065,6 +1092,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Vue invité"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Pour activer la vue invité, veuillez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Nous ne suivons pas les installations d\'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1081,7 +1110,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Masquer les éléments partagés avec vous dans la galerie"), "hiding": MessageLookupByLibrary.simpleMessage("Masquage en cours..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hébergé chez OSM France"), "howItWorks": @@ -1141,7 +1170,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Il semble qu\'une erreur s\'est produite. Veuillez réessayer après un certain temps. Si l\'erreur persiste, veuillez contacter notre équipe d\'assistance."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Les éléments montrent le nombre de jours restants avant la suppression définitive"), @@ -1163,7 +1192,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Merci de nous aider avec cette information"), "language": MessageLookupByLibrary.simpleMessage("Langue"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Dernière mise à jour"), "lastYearsTrip": @@ -1178,7 +1207,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Héritage"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Comptes hérités"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "L\'héritage permet aux contacts de confiance d\'accéder à votre compte en votre absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1195,7 +1224,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("pour un partage plus rapide"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activé"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expiré"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiration du lien"), "linkHasExpired": @@ -1204,13 +1233,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Lier la personne"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "pour une meilleure expérience de partage"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Photos en direct"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Vous pouvez partager votre abonnement avec votre famille"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Nous avons conservé plus de 30 millions de souvenirs jusqu\'à présent"), + "Nous avons préservé plus de 200 millions de souvenirs jusqu\'à présent"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Nous conservons 3 copies de vos données, l\'une dans un abri anti-atomique"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1267,6 +1296,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Appuyez longuement sur un élément pour le voir en plein écran"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Vidéo en boucle désactivée"), "loopVideoOn": @@ -1296,6 +1327,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Moi"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Boutique"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Fusionner avec existant"), @@ -1329,13 +1361,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Les plus pertinents"), "mountains": MessageLookupByLibrary.simpleMessage("Au-dessus des collines"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Déplacer les photos sélectionnées vers une date"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Déplacer vers l\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Déplacer vers un album masqué"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Déplacé dans la corbeille"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1350,6 +1383,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Nouvel album"), "newLocation": MessageLookupByLibrary.simpleMessage("Nouveau lieu"), "newPerson": MessageLookupByLibrary.simpleMessage("Nouvelle personne"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Nouvelle plage"), "newToEnte": MessageLookupByLibrary.simpleMessage("Nouveau sur Ente"), "newest": MessageLookupByLibrary.simpleMessage("Le plus récent"), @@ -1389,10 +1423,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Aucun résultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Aucun résultat trouvé"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Aucun verrou système trouvé"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage( "Ce n\'est pas cette personne ?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1406,7 +1440,10 @@ class MessageLookup extends MessageLookupByLibrary { "Sur Ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("De nouveau sur la route"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Seulement eux"), "oops": MessageLookupByLibrary.simpleMessage("Oups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1438,7 +1475,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Appairage terminé"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "La vérification est toujours en attente"), "passkey": MessageLookupByLibrary.simpleMessage( @@ -1450,7 +1487,7 @@ class MessageLookup extends MessageLookupByLibrary { "Le mot de passe a été modifié"), "passwordLock": MessageLookupByLibrary.simpleMessage( "Verrouillage par mot de passe"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La force du mot de passe est calculée en tenant compte de la longueur du mot de passe, des caractères utilisés et du fait que le mot de passe figure ou non parmi les 10 000 mots de passe les plus utilisés"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1461,7 +1498,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Échec du paiement"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Malheureusement votre paiement a échoué. Veuillez contacter le support et nous vous aiderons !"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Éléments en attente"), "pendingSync": @@ -1475,10 +1512,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Supprimer définitivement"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Supprimer définitivement de l\'appareil ?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nom de la personne"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Compagnons à quatre pattes"), "photoDescriptions": @@ -1486,11 +1523,12 @@ class MessageLookup extends MessageLookupByLibrary { "photoGridSize": MessageLookupByLibrary.simpleMessage("Taille de la grille photo"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("photo"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Photos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Les photos ajoutées par vous seront retirées de l\'album"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Les photos gardent une différence de temps relative"), @@ -1503,7 +1541,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Lire l\'album sur la TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Lire l\'original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Lire le stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"), @@ -1516,14 +1554,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Merci de contacter l\'assistance si cette erreur persiste"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Veuillez accorder la permission"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Veuillez vous reconnecter"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Veuillez sélectionner les liens rapides à supprimer"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Veuillez réessayer"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1538,7 +1576,7 @@ class MessageLookup extends MessageLookupByLibrary { "Veuillez attendre quelque temps avant de réessayer"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Veuillez patienter, cela prendra un peu de temps."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Préparation des journaux..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Conserver plus"), @@ -1558,7 +1596,7 @@ class MessageLookup extends MessageLookupByLibrary { "processed": MessageLookupByLibrary.simpleMessage("Appris"), "processing": MessageLookupByLibrary.simpleMessage("Traitement en cours"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Traitement des vidéos"), "publicLinkCreated": @@ -1572,12 +1610,14 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Évaluer l\'application"), "rateUs": MessageLookupByLibrary.simpleMessage("Évaluez-nous"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Réassigner \"Moi\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Réassignation..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Récupérer"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Récupérer un compte"), @@ -1586,7 +1626,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Récupérer un compte"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Récupération initiée"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clé de secours"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Clé de secours copiée dans le presse-papiers"), @@ -1600,12 +1640,12 @@ class MessageLookup extends MessageLookupByLibrary { "Clé de récupération vérifiée"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Votre clé de récupération est la seule façon de récupérer vos photos si vous oubliez votre mot de passe. Vous pouvez trouver votre clé de récupération dans Paramètres > Compte.\n\nVeuillez saisir votre clé de récupération ici pour vous assurer de l\'avoir enregistré correctement."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Restauration réussie !"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de confiance tente d\'accéder à votre compte"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "L\'appareil actuel n\'est pas assez puissant pour vérifier votre mot de passe, mais nous pouvons le régénérer d\'une manière qui fonctionne avec tous les appareils.\n\nVeuillez vous connecter à l\'aide de votre clé de secours et régénérer votre mot de passe (vous pouvez réutiliser le même si vous le souhaitez)."), "recreatePasswordTitle": @@ -1621,7 +1661,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Donnez ce code à vos ami·e·s"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ils souscrivent à une offre payante"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Parrainages"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Les recommandations sont actuellement en pause"), @@ -1653,7 +1693,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Supprimer le participant"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Supprimer le libellé d\'une personne"), "removePublicLink": @@ -1675,7 +1715,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Renommer le fichier"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renouveler l’abonnement"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), "resendEmail": @@ -1701,7 +1741,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Examiner les suggestions"), "right": MessageLookupByLibrary.simpleMessage("Droite"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Pivoter"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Pivoter à gauche"), "rotateRight": @@ -1761,8 +1801,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invitez quelqu\'un·e et vous verrez ici toutes les photos partagées"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Les personnes seront affichées ici une fois le traitement terminé"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ouvrir les liens des albums publics dans l\'application"), @@ -1815,9 +1855,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront retirés de cette personne, mais pas supprimés de votre bibliothèque."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Envoyer"), "sendEmail": MessageLookupByLibrary.simpleMessage("Envoyer un e-mail"), "sendInvite": @@ -1851,16 +1891,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Partagez un album maintenant"), "shareLink": MessageLookupByLibrary.simpleMessage("Partager le lien"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partagez uniquement avec les personnes que vous souhaitez"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Téléchargez Ente pour pouvoir facilement partager des photos et vidéos en qualité originale\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partager avec des utilisateurs non-Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partagez votre premier album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1871,7 +1911,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nouvelles photos partagées"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recevoir des notifications quand quelqu\'un·e ajoute une photo à un album partagé dont vous faites partie"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partagés avec moi"), "sharedWithYou": @@ -1880,7 +1920,7 @@ class MessageLookup extends MessageLookupByLibrary { "shiftDatesAndTime": MessageLookupByLibrary.simpleMessage("Dates et heure de décalage"), "showMemories": - MessageLookupByLibrary.simpleMessage("Montrer les souvenirs"), + MessageLookupByLibrary.simpleMessage("Afficher les souvenirs"), "showPerson": MessageLookupByLibrary.simpleMessage("Montrer la personne"), "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage( @@ -1891,11 +1931,11 @@ class MessageLookup extends MessageLookupByLibrary { "Déconnecter les autres appareils"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "J\'accepte les conditions d\'utilisation et la politique de confidentialité"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Elle sera supprimée de tous les albums."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Ignorer"), "social": MessageLookupByLibrary.simpleMessage("Retrouvez nous"), "someItemsAreInBothEnteAndYourDevice": @@ -1913,6 +1953,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Quelque chose s\'est mal passé, veuillez recommencer"), "sorry": MessageLookupByLibrary.simpleMessage("Désolé"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Désolé, nous n\'avons pas pu sauvegarder ce fichier maintenant, nous allons réessayer plus tard."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Désolé, impossible d\'ajouter aux favoris !"), "sorryCouldNotRemoveFromFavorites": @@ -1924,6 +1966,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Désolé, nous n\'avons pas pu générer de clés sécurisées sur cet appareil.\n\nVeuillez vous inscrire depuis un autre appareil."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Trier"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Trier par"), "sortNewestFirst": @@ -1931,8 +1975,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Plus ancien en premier"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Succès"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Éclairage sur vous-même"), "startAccountRecoveryTitle": @@ -1947,15 +1991,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Stockage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famille"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Vous"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limite de stockage atteinte"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Détails du stream"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("S\'abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Vous avez besoin d\'un abonnement payant actif pour activer le partage."), @@ -1973,7 +2017,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Suggérer une fonctionnalité"), "sunrise": MessageLookupByLibrary.simpleMessage("À l\'horizon"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisation arrêtée ?"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1986,7 +2030,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Appuyer pour déverrouiller"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Appuyer pour envoyer"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Il semble qu\'une erreur s\'est produite. Veuillez réessayer après un certain temps. Si l\'erreur persiste, veuillez contacter notre équipe d\'assistance."), "terminate": MessageLookupByLibrary.simpleMessage("Se déconnecter"), @@ -2010,7 +2054,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ces éléments seront supprimés de votre appareil."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ils seront supprimés de tous les albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2028,12 +2072,12 @@ class MessageLookup extends MessageLookupByLibrary { "Cette image n\'a pas de données exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("C\'est moi !"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ceci est votre ID de vérification"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Cette semaine au fil des années"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Cela vous déconnectera de l\'appareil suivant :"), @@ -2045,7 +2089,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Ceci supprimera les liens publics de tous les liens rapides sélectionnés."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Pour activer le verrouillage de l\'application vous devez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."), @@ -2059,13 +2103,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"), "trash": MessageLookupByLibrary.simpleMessage("Corbeille"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Recadrer"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacts de confiance"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Réessayer"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activez la sauvegarde pour charger automatiquement sur Ente les fichiers ajoutés à ce dossier de l\'appareil."), @@ -2085,7 +2129,7 @@ class MessageLookup extends MessageLookupByLibrary { "L\'authentification à deux facteurs a été réinitialisée avec succès "), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuration de l\'authentification à deux facteurs"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Désarchiver"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Désarchiver l\'album"), @@ -2113,10 +2157,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Mise à jour de la sélection du dossier..."), "upgrade": MessageLookupByLibrary.simpleMessage("Améliorer"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Envoi des fichiers vers l\'album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Sauvegarde d\'un souvenir..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2134,7 +2178,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Utiliser la photo sélectionnée"), "usedSpace": MessageLookupByLibrary.simpleMessage("Stockage utilisé"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "La vérification a échouée, veuillez réessayer"), @@ -2143,7 +2187,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Vérifier l\'email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Vérifier la clé de sécurité"), @@ -2156,7 +2200,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Informations vidéo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vidéo"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Streaming vidéo"), + MessageLookupByLibrary.simpleMessage("Vidéos diffusables"), "videos": MessageLookupByLibrary.simpleMessage("Vidéos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage( "Afficher les connexions actives"), @@ -2171,9 +2215,11 @@ class MessageLookup extends MessageLookupByLibrary { "Affichez les fichiers qui consomment le plus de stockage."), "viewLogs": MessageLookupByLibrary.simpleMessage("Afficher les journaux"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Voir la clé de récupération"), "viewer": MessageLookupByLibrary.simpleMessage("Observateur"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Vous pouvez gérer votre abonnement sur web.ente.io"), "waitingForVerification": MessageLookupByLibrary.simpleMessage( @@ -2186,7 +2232,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nous ne prenons pas en charge l\'édition des photos et des albums que vous ne possédez pas encore"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Securité Faible"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bienvenue !"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nouveautés"), @@ -2194,7 +2240,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contact de confiance peut vous aider à récupérer vos données."), "yearShort": MessageLookupByLibrary.simpleMessage("an"), "yearly": MessageLookupByLibrary.simpleMessage("Annuel"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Oui"), "yesCancel": MessageLookupByLibrary.simpleMessage("Oui, annuler"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2209,7 +2255,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Oui, réinitialiser la personne"), "you": MessageLookupByLibrary.simpleMessage("Vous"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Vous êtes sur un plan familial !"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2228,7 +2274,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vous ne pouvez pas partager avec vous-même"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Vous n\'avez aucun élément archivé."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Votre compte a été supprimé"), "yourMap": MessageLookupByLibrary.simpleMessage("Votre carte"), diff --git a/mobile/lib/generated/intl/messages_gu.dart b/mobile/lib/generated/intl/messages_gu.dart index 6c1d7e4d90..6d7c763bdf 100644 --- a/mobile/lib/generated/intl/messages_gu.dart +++ b/mobile/lib/generated/intl/messages_gu.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'gu'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_he.dart b/mobile/lib/generated/intl/messages_he.dart index 8799cd791c..c254498ecb 100644 --- a/mobile/lib/generated/intl/messages_he.dart +++ b/mobile/lib/generated/intl/messages_he.dart @@ -57,67 +57,67 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} קבצים, כל אחד ${formattedSize}"; - static String m29(email) => + static String m30(email) => "לא נמצא חשבון ente ל-${email}.\n\nשלח להם הזמנה על מנת לשתף תמונות."; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB כל פעם שמישהו נרשם עבור תוכנית בתשלום ומחיל את הקוד שלך"; - static String m36(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; + static String m37(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} פריט', two: '${count} פריטים', many: '${count} פריטים', other: '${count} פריטים')}"; - static String m45(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; + static String m46(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "חוזק הסיסמא: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "אנא דבר עם התמיכה של ${providerName} אם אתה חוייבת"; - static String m66(storeName) => "דרג אותנו ב-${storeName}"; + static String m67(storeName) => "דרג אותנו ב-${storeName}"; - static String m71(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; + static String m72(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} יוסר מהאלבום המשותף הזה\n\nגם תמונות שנוספו על ידיהם יוסרו מהאלבום"; - static String m76(count) => "${count} נבחרו"; + static String m78(count) => "${count} נבחרו"; - static String m77(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; + static String m79(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; - static String m79(verificationID) => + static String m81(verificationID) => "הנה מזהה האימות שלי: ${verificationID} עבור ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "היי, תוכל לוודא שזה מזהה האימות שלך של ente.io: ${verificationID}"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'שתף עם אנשים ספציפיים', one: 'שותף עם איש 1', two: 'שותף עם 2 אנשים', other: 'שותף עם ${numberOfPeople} אנשים')}"; - static String m83(emailIDs) => "הושתף ע\"י ${emailIDs}"; + static String m85(emailIDs) => "הושתף ע\"י ${emailIDs}"; - static String m84(fileType) => "${fileType} יימחק מהמכשיר שלך."; + static String m86(fileType) => "${fileType} יימחק מהמכשיר שלך."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m92(endDate) => "המנוי שלך יבוטל ב-${endDate}"; + static String m94(endDate) => "המנוי שלך יבוטל ב-${endDate}"; - static String m93(completed, total) => "${completed}/${total} זכרונות נשמרו"; + static String m95(completed, total) => "${completed}/${total} זכרונות נשמרו"; - static String m95(storageAmountInGB) => "הם גם יקבלו ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "הם גם יקבלו ${storageAmountInGB} GB"; - static String m96(email) => "זה מזהה האימות של ${email}"; + static String m98(email) => "זה מזהה האימות של ${email}"; - static String m107(email) => "אמת ${email}"; + static String m109(email) => "אמת ${email}"; - static String m109(email) => "שלחנו דוא\"ל ל${email}"; + static String m112(email) => "שלחנו דוא\"ל ל${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'לפני ${count} שנה', two: 'לפני ${count} שנים', many: 'לפני ${count} שנים', other: 'לפני ${count} שנים')}"; - static String m112(storageSaved) => "הצלחת לפנות ${storageSaved}!"; + static String m115(storageSaved) => "הצלחת לפנות ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -223,6 +223,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("גבה על רשת סלולרית"), "backupSettings": MessageLookupByLibrary.simpleMessage("הגדרות גיבוי"), "backupVideos": MessageLookupByLibrary.simpleMessage("גבה סרטונים"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blog": MessageLookupByLibrary.simpleMessage("בלוג"), "cachedData": MessageLookupByLibrary.simpleMessage("נתונים מוטמנים"), "canNotUploadToAlbumsOwnedByOthers": @@ -254,6 +255,8 @@ class MessageLookup extends MessageLookupByLibrary { "claimed": MessageLookupByLibrary.simpleMessage("נתבע"), "claimedStorageSoFar": m14, "click": MessageLookupByLibrary.simpleMessage("• לחץ"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("סגור"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("קבץ לפי זמן הצילום"), @@ -396,7 +399,7 @@ class MessageLookup extends MessageLookupByLibrary { "edit": MessageLookupByLibrary.simpleMessage("ערוך"), "eligible": MessageLookupByLibrary.simpleMessage("זכאי"), "email": MessageLookupByLibrary.simpleMessage("דוא\"ל"), - "emailNoEnteAccount": m29, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("אימות מייל"), "empty": MessageLookupByLibrary.simpleMessage("ריק"), @@ -465,11 +468,11 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("שכחתי סיסמה"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("מקום אחסון בחינם נתבע"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("מקום אחסון שמיש"), "freeTrial": MessageLookupByLibrary.simpleMessage("ניסיון חינמי"), - "freeTrialValidTill": m36, + "freeTrialValidTill": m37, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("פנה אחסון במכשיר"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("פנה מקום"), @@ -480,6 +483,8 @@ class MessageLookup extends MessageLookupByLibrary { "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( "נא לתת גישה לכל התמונות בתוך ההגדרות של הטלפון"), "grantPermission": MessageLookupByLibrary.simpleMessage("הענק הרשאה"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hidden": MessageLookupByLibrary.simpleMessage("מוסתר"), "hide": MessageLookupByLibrary.simpleMessage("הסתר"), "hiding": MessageLookupByLibrary.simpleMessage("מחביא..."), @@ -507,7 +512,7 @@ class MessageLookup extends MessageLookupByLibrary { "invite": MessageLookupByLibrary.simpleMessage("הזמן"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("הזמן את חברייך"), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "הפריטים שנבחרו יוסרו מהאלבום הזה"), "keepPhotos": MessageLookupByLibrary.simpleMessage("השאר תמונות"), @@ -529,7 +534,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("מגבלת כמות מכשירים"), "linkEnabled": MessageLookupByLibrary.simpleMessage("מאופשר"), "linkExpired": MessageLookupByLibrary.simpleMessage("פג תוקף"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("תאריך תפוגה ללינק"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("הקישור פג תוקף"), @@ -545,6 +550,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "לחץ לחיצה ארוכה על פריט על מנת לראות אותו במסך מלא"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("איבדת את המכשיר?"), "manage": MessageLookupByLibrary.simpleMessage("נהל"), "manageFamily": MessageLookupByLibrary.simpleMessage("נהל משפחה"), @@ -567,6 +574,7 @@ class MessageLookup extends MessageLookupByLibrary { "name": MessageLookupByLibrary.simpleMessage("שם"), "never": MessageLookupByLibrary.simpleMessage("לעולם לא"), "newAlbum": MessageLookupByLibrary.simpleMessage("אלבום חדש"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newest": MessageLookupByLibrary.simpleMessage("החדש ביותר"), "no": MessageLookupByLibrary.simpleMessage("לא"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("אין"), @@ -586,6 +594,9 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("על המכשיר"), "onEnte": MessageLookupByLibrary.simpleMessage("באנטע"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("אופס"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("אופס, משהו השתבש"), @@ -598,12 +609,12 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("הססמה הוחלפה בהצלחה"), "passwordLock": MessageLookupByLibrary.simpleMessage("נעילת סיסמא"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "אנחנו לא שומרים את הסיסמא הזו, לכן אם אתה שוכח אותה, אנחנו לא יכולים לפענח את המידע שלך"), "paymentDetails": MessageLookupByLibrary.simpleMessage("פרטי תשלום"), "paymentFailed": MessageLookupByLibrary.simpleMessage("התשלום נכשל"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("אנשים משתמשים בקוד שלך"), "permanentlyDelete": @@ -645,7 +656,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("צור ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("דרג את האפליקציה"), "rateUs": MessageLookupByLibrary.simpleMessage("דרג אותנו"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("שחזר"), "recoverAccount": MessageLookupByLibrary.simpleMessage("שחזר חשבון"), "recoverButton": MessageLookupByLibrary.simpleMessage("שחזר"), @@ -671,7 +684,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. תמסור את הקוד הזה לחברייך"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. הם נרשמים עבור תוכנית בתשלום"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("הפניות"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("הפניות כרגע מושהות"), @@ -687,7 +700,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("הסר מהאלבום?"), "removeLink": MessageLookupByLibrary.simpleMessage("הסרת קישור"), "removeParticipant": MessageLookupByLibrary.simpleMessage("הסר משתתף"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePublicLink": MessageLookupByLibrary.simpleMessage("הסר לינק ציבורי"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -738,8 +751,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "התיקיות שנבחרו יוצפנו ויגובו"), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("שלח"), "sendEmail": MessageLookupByLibrary.simpleMessage("שלח דוא\"ל"), "sendInvite": MessageLookupByLibrary.simpleMessage("שלח הזמנה"), @@ -758,15 +771,15 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("שתף אלבום עכשיו"), "shareLink": MessageLookupByLibrary.simpleMessage("שתף קישור"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("שתף רק אם אנשים שאתה בוחר"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "הורד את ente על מנת שנוכל לשתף תמונות וסרטונים באיכות המקור באופן קל\n\nhttps://ente.io"), "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "שתף עם משתמשים שהם לא של ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("שתף את האלבום הראשון שלך"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -777,13 +790,13 @@ class MessageLookup extends MessageLookupByLibrary { "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "קבל התראות כשמישהו מוסיף תמונה לאלבום משותף שאתה חלק ממנו"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("שותף איתי"), "sharing": MessageLookupByLibrary.simpleMessage("משתף..."), "showMemories": MessageLookupByLibrary.simpleMessage("הצג זכרונות"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "אני מסכים לתנאי שירות ולמדיניות הפרטיות"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("זה יימחק מכל האלבומים."), "skip": MessageLookupByLibrary.simpleMessage("דלג"), @@ -804,6 +817,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "אנחנו מצטערים, לא הצלחנו ליצור מפתחות מאובטחים על מכשיר זה.\n\nאנא הירשם ממכשיר אחר."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("מיין לפי"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("הישן ביותר קודם"), @@ -812,18 +827,18 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("אחסון"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("משפחה"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("אתה"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("גבול מקום האחסון נחרג"), "strongStrength": MessageLookupByLibrary.simpleMessage("חזקה"), - "subWillBeCancelledOn": m92, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("הרשם"), "subscription": MessageLookupByLibrary.simpleMessage("מנוי"), "success": MessageLookupByLibrary.simpleMessage("הצלחה"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("הציעו מאפיינים"), "support": MessageLookupByLibrary.simpleMessage("תמיכה"), - "syncProgress": m93, + "syncProgress": m95, "syncing": MessageLookupByLibrary.simpleMessage("מסנכרן..."), "systemTheme": MessageLookupByLibrary.simpleMessage("מערכת"), "tapToCopy": MessageLookupByLibrary.simpleMessage("הקש כדי להעתיק"), @@ -839,12 +854,12 @@ class MessageLookup extends MessageLookupByLibrary { "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage("לא ניתן להשלים את ההורדה"), "theme": MessageLookupByLibrary.simpleMessage("ערכת נושא"), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "זה יכול לשמש לשחזור החשבון שלך במקרה ותאבד את הגורם השני"), "thisDevice": MessageLookupByLibrary.simpleMessage("מכשיר זה"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("זה מזהה האימות שלך"), "thisWillLogYouOutOfTheFollowingDevice": @@ -888,7 +903,7 @@ class MessageLookup extends MessageLookupByLibrary { "verificationId": MessageLookupByLibrary.simpleMessage("מזהה אימות"), "verify": MessageLookupByLibrary.simpleMessage("אמת"), "verifyEmail": MessageLookupByLibrary.simpleMessage("אימות דוא\"ל"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("אמת"), "verifyPassword": MessageLookupByLibrary.simpleMessage("אמת סיסמא"), "verifyingRecoveryKey": @@ -905,11 +920,11 @@ class MessageLookup extends MessageLookupByLibrary { "אנא בקר ב-web.ente.io על מנת לנהל את המנוי שלך"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("הקוד שלנו פתוח!"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("חלשה"), "welcomeBack": MessageLookupByLibrary.simpleMessage("ברוך שובך!"), "yearly": MessageLookupByLibrary.simpleMessage("שנתי"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("כן"), "yesCancel": MessageLookupByLibrary.simpleMessage("כן, בטל"), "yesConvertToViewer": @@ -932,7 +947,7 @@ class MessageLookup extends MessageLookupByLibrary { "אתה לא יכול לשנמך לתוכנית הזו"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage("אתה לא יכול לשתף עם עצמך"), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("החשבון שלך נמחק"), "yourPlanWasSuccessfullyDowngraded": diff --git a/mobile/lib/generated/intl/messages_hi.dart b/mobile/lib/generated/intl/messages_hi.dart index ff4756d8d4..202db74c87 100644 --- a/mobile/lib/generated/intl/messages_hi.dart +++ b/mobile/lib/generated/intl/messages_hi.dart @@ -27,7 +27,10 @@ class MessageLookup extends MessageLookupByLibrary { "activeSessions": MessageLookupByLibrary.simpleMessage("एक्टिव सेशन"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "आपका अकाउंट हटाने का मुख्य कारण क्या है?"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "cancel": MessageLookupByLibrary.simpleMessage("रद्द करें"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "अकाउंट डिलीट करने की पुष्टि करें"), "confirmPassword": @@ -67,6 +70,8 @@ class MessageLookup extends MessageLookupByLibrary { "feedback": MessageLookupByLibrary.simpleMessage("प्रतिपुष्टि"), "forgotPassword": MessageLookupByLibrary.simpleMessage("पासवर्ड भूल गए"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage( "आपके द्वारा दर्ज रिकवरी कुंजी ग़लत है"), "incorrectRecoveryKeyTitle": @@ -75,13 +80,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("अमान्य ईमेल ऐड्रेस"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "कृपया हमें इस जानकारी के लिए सहायता करें"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "noRecoveryKey": MessageLookupByLibrary.simpleMessage("रिकवरी कुंजी नहीं है?"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( "हमारे एंड-टू-एंड एन्क्रिप्शन प्रोटोकॉल की प्रकृति के कारण, आपके डेटा को आपके पासवर्ड या रिकवरी कुंजी के बिना डिक्रिप्ट नहीं किया जा सकता है"), "ok": MessageLookupByLibrary.simpleMessage("ठीक है"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("ओह!"), "password": MessageLookupByLibrary.simpleMessage("पासवर्ड"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recoverButton": MessageLookupByLibrary.simpleMessage("पुनः प्राप्त"), "recoverySuccessful": MessageLookupByLibrary.simpleMessage("रिकवरी सफल हुई!"), @@ -91,6 +104,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "कुछ गड़बड़ हुई है। कृपया दोबारा प्रयास करें।"), "sorry": MessageLookupByLibrary.simpleMessage("क्षमा करें!"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "terminate": MessageLookupByLibrary.simpleMessage("रद्द करें"), "terminateSession": MessageLookupByLibrary.simpleMessage("सेशन रद्द करें?"), diff --git a/mobile/lib/generated/intl/messages_hu.dart b/mobile/lib/generated/intl/messages_hu.dart index fbe4a79ba1..9af5b27648 100644 --- a/mobile/lib/generated/intl/messages_hu.dart +++ b/mobile/lib/generated/intl/messages_hu.dart @@ -26,7 +26,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Köszöntjük ismét!"), "askDeleteReason": MessageLookupByLibrary.simpleMessage("Miért törli a fiókját?"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "cancel": MessageLookupByLibrary.simpleMessage("Mégse"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Fiók törlése"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( "Sajnáljuk, hogy távozik. Kérjük, ossza meg velünk visszajelzéseit, hogy segítsen nekünk a fejlődésben."), @@ -36,8 +39,20 @@ class MessageLookup extends MessageLookupByLibrary { "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Adja meg az e-mail címét"), "feedback": MessageLookupByLibrary.simpleMessage("Visszajelzés"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Érvénytelen e-mail cím"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "verify": MessageLookupByLibrary.simpleMessage("Hitelesítés") }; } diff --git a/mobile/lib/generated/intl/messages_id.dart b/mobile/lib/generated/intl/messages_id.dart index adf62166f0..9b635958f0 100644 --- a/mobile/lib/generated/intl/messages_id.dart +++ b/mobile/lib/generated/intl/messages_id.dart @@ -74,117 +74,117 @@ class MessageLookup extends MessageLookupByLibrary { static String m25(count, storageSaved) => "Kamu telah menghapus ${Intl.plural(count, other: '${count} file duplikat')} dan membersihkan (${storageSaved}!)"; - static String m27(newEmail) => "Email diubah menjadi ${newEmail}"; + static String m28(newEmail) => "Email diubah menjadi ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} tidak punya akun Ente.\n\nUndang dia untuk berbagi foto."; - static String m33(count, formattedNumber) => + static String m34(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} di perangkat ini telah berhasil dicadangkan"; - static String m34(count, formattedNumber) => + static String m35(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} dalam album ini telah berhasil dicadangkan"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB setiap kali orang mendaftar dengan paket berbayar lalu menerapkan kode milikmu"; - static String m36(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; + static String m37(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; - static String m38(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Memproses ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} item')}"; + static String m43(count) => "${Intl.plural(count, other: '${count} item')}"; - static String m45(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; + static String m46(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; - static String m50(albumName) => "Berhasil dipindahkan ke ${albumName}"; + static String m51(albumName) => "Berhasil dipindahkan ke ${albumName}"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Harap hubungi ${familyAdminEmail} untuk mengubah kode kamu."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Keamanan sandi: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Harap hubungi dukungan ${providerName} jika kamu dikenai biaya"; - static String m61(endDate) => + static String m62(endDate) => "Percobaan gratis berlaku hingga ${endDate}.\nKamu dapat memilih paket berbayar setelahnya."; - static String m62(toEmail) => "Silakan kirimi kami email di ${toEmail}"; + static String m63(toEmail) => "Silakan kirimi kami email di ${toEmail}"; - static String m63(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; + static String m64(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; - static String m66(storeName) => "Beri nilai di ${storeName}"; + static String m67(storeName) => "Beri nilai di ${storeName}"; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Kalian berdua mendapat ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} akan dikeluarkan dari album berbagi ini\n\nSemua foto yang ia tambahkan juga akan dihapus dari album ini"; - static String m73(endDate) => "Langganan akan diperpanjang pada ${endDate}"; + static String m74(endDate) => "Langganan akan diperpanjang pada ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, other: '${count} hasil ditemukan')}"; - static String m76(count) => "${count} terpilih"; + static String m78(count) => "${count} terpilih"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} dipilih (${yourCount} milikmu)"; - static String m79(verificationID) => + static String m81(verificationID) => "Ini ID Verifikasi saya di ente.io: ${verificationID}."; - static String m80(verificationID) => + static String m82(verificationID) => "Halo, bisakah kamu pastikan bahwa ini adalah ID Verifikasi ente.io milikmu: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Kode rujukan Ente: ${referralCode} \n\nTerapkan pada Pengaturan → Umum → Rujukan untuk mendapatkan ${referralStorageInGB} GB gratis setelah kamu mendaftar paket berbayar\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bagikan dengan orang tertentu', one: 'Berbagi dengan 1 orang', other: 'Berbagi dengan ${numberOfPeople} orang')}"; - static String m83(emailIDs) => "Dibagikan dengan ${emailIDs}"; + static String m85(emailIDs) => "Dibagikan dengan ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "${fileType} ini akan dihapus dari perangkat ini."; - static String m85(fileType) => + static String m87(fileType) => "${fileType} ini tersimpan di Ente dan juga di perangkat ini."; - static String m86(fileType) => "${fileType} ini akan dihapus dari Ente."; + static String m88(fileType) => "${fileType} ini akan dihapus dari Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} dari ${totalAmount} ${totalStorageUnit} terpakai"; - static String m91(id) => + static String m93(id) => "${id} kamu telah terhubung dengan akun Ente lain.\nJika kamu ingin menggunakan ${id} kamu untuk akun ini, silahkan hubungi tim bantuan kami"; - static String m92(endDate) => + static String m94(endDate) => "Langganan kamu akan dibatalkan pada ${endDate}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Ia juga mendapat ${storageAmountInGB} GB"; - static String m96(email) => "Ini adalah ID Verifikasi milik ${email}"; + static String m98(email) => "Ini adalah ID Verifikasi milik ${email}"; - static String m106(endDate) => "Berlaku hingga ${endDate}"; + static String m108(endDate) => "Berlaku hingga ${endDate}"; - static String m107(email) => "Verifikasi ${email}"; + static String m109(email) => "Verifikasi ${email}"; - static String m109(email) => + static String m112(email) => "Kami telah mengirimkan email ke ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, other: '${count} tahun lalu')}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Kamu telah berhasil membersihkan ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -342,11 +342,14 @@ class MessageLookup extends MessageLookupByLibrary { "backupStatusDescription": MessageLookupByLibrary.simpleMessage( "Item yang sudah dicadangkan akan terlihat di sini"), "backupVideos": MessageLookupByLibrary.simpleMessage("Cadangkan video"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Penawaran Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), "cachedData": MessageLookupByLibrary.simpleMessage("Data cache"), "calculating": MessageLookupByLibrary.simpleMessage("Menghitung..."), + "canNotOpenTitle": MessageLookupByLibrary.simpleMessage( + "Tidak dapat membuka album ini"), "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Hanya dapat menghapus berkas yang dimiliki oleh mu"), "cancel": MessageLookupByLibrary.simpleMessage("Batal"), @@ -386,6 +389,8 @@ class MessageLookup extends MessageLookupByLibrary { "claimedStorageSoFar": m14, "clearIndexes": MessageLookupByLibrary.simpleMessage("Hapus indeks"), "click": MessageLookupByLibrary.simpleMessage("• Click"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Tutup"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Kode diterapkan"), @@ -404,6 +409,7 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Kolaborator bisa menambahkan foto dan video ke album bersama ini."), + "collageLayout": MessageLookupByLibrary.simpleMessage("Tata letak"), "collectEventPhotos": MessageLookupByLibrary.simpleMessage("Kumpulkan foto acara"), "collectPhotos": MessageLookupByLibrary.simpleMessage("Kumpulkan foto"), @@ -584,8 +590,10 @@ class MessageLookup extends MessageLookupByLibrary { "Perubahan lokasi hanya akan terlihat di Ente"), "eligible": MessageLookupByLibrary.simpleMessage("memenuhi syarat"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailAlreadyRegistered": + MessageLookupByLibrary.simpleMessage("Email sudah terdaftar."), + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifikasi email"), "empty": MessageLookupByLibrary.simpleMessage("Kosongkan"), @@ -633,6 +641,8 @@ class MessageLookup extends MessageLookupByLibrary { "Harap masukkan alamat email yang sah."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Masukkan alamat email kamu"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Masukkan alamat email baru anda"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Masukkan sandi kamu"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -672,6 +682,7 @@ class MessageLookup extends MessageLookupByLibrary { "familyPlans": MessageLookupByLibrary.simpleMessage("Paket keluarga"), "faq": MessageLookupByLibrary.simpleMessage("Tanya Jawab Umum"), "faqs": MessageLookupByLibrary.simpleMessage("Tanya Jawab Umum"), + "favorite": MessageLookupByLibrary.simpleMessage("Favorit"), "feedback": MessageLookupByLibrary.simpleMessage("Masukan"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( "Gagal menyimpan file ke galeri"), @@ -682,8 +693,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Jenis file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Nama dan jenis file"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("File terhapus"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File tersimpan ke galeri"), @@ -697,12 +708,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Wajah yang ditemukan"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Kuota gratis diperoleh"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Kuota gratis yang dapat digunakan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Percobaan gratis"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Bersihkan penyimpanan perangkat"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -711,7 +722,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Umum"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Menghasilkan kunci enkripsi..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Buka pengaturan"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -719,6 +730,8 @@ class MessageLookup extends MessageLookupByLibrary { "grantPermission": MessageLookupByLibrary.simpleMessage("Berikan izin"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage( "Kelompokkan foto yang berdekatan"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "Dari mana Anda menemukan Ente? (opsional)"), "help": MessageLookupByLibrary.simpleMessage("Bantuan"), @@ -771,7 +784,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Sepertinya terjadi kesalahan. Silakan coba lagi setelah beberapa saat. Jika kesalahan terus terjadi, silakan hubungi tim dukungan kami."), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Item yang dipilih akan dihapus dari album ini"), "joinDiscord": @@ -798,7 +811,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Batas perangkat"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktif"), "linkExpired": MessageLookupByLibrary.simpleMessage("Kedaluwarsa"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Waktu kedaluwarsa link"), "linkHasExpired": @@ -807,8 +820,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tidak pernah"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Kamu bisa membagikan langgananmu dengan keluarga"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Kami telah memelihara lebih dari 30 juta kenangan saat ini"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Kami menyimpan 3 salinan dari data kamu, salah satunya di tempat pengungsian bawah tanah"), "loadMessage7": MessageLookupByLibrary.simpleMessage( @@ -842,6 +853,8 @@ class MessageLookup extends MessageLookupByLibrary { "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( "Tekan dan tahan email untuk membuktikan enkripsi ujung ke ujung."), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("Perangkat hilang?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Pemelajaran mesin"), @@ -877,9 +890,11 @@ class MessageLookup extends MessageLookupByLibrary { "moderateStrength": MessageLookupByLibrary.simpleMessage("Sedang"), "moments": MessageLookupByLibrary.simpleMessage("Momen"), "monthly": MessageLookupByLibrary.simpleMessage("Bulanan"), + "moveToAlbum": + MessageLookupByLibrary.simpleMessage("Pindahkan ke album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Pindahkan ke album tersembunyi"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Pindah ke sampah"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -891,6 +906,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tidak dapat terhubung dengan Ente, harap periksa pengaturan jaringan kamu dan hubungi dukungan jika masalah berlanjut."), "never": MessageLookupByLibrary.simpleMessage("Tidak pernah"), "newAlbum": MessageLookupByLibrary.simpleMessage("Album baru"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Baru di Ente"), "newest": MessageLookupByLibrary.simpleMessage("Terbaru"), "no": MessageLookupByLibrary.simpleMessage("Tidak"), @@ -930,7 +946,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Di perangkat ini"), "onEnte": MessageLookupByLibrary.simpleMessage( "Di ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "oops": MessageLookupByLibrary.simpleMessage("Aduh"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Aduh, tidak dapat menyimpan perubahan"), @@ -957,7 +976,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sandi berhasil diubah"), "passwordLock": MessageLookupByLibrary.simpleMessage("Kunci dengan sandi"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Kami tidak menyimpan sandi ini, jadi jika kamu melupakannya, kami tidak akan bisa mendekripsi data kamu"), "paymentDetails": @@ -966,7 +985,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pembayaran gagal"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Sayangnya, pembayaranmu gagal. Silakan hubungi tim bantuan agar dapat kami bantu!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Item menunggu"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sinkronisasi tertunda"), @@ -989,7 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Foto yang telah kamu tambahkan akan dihapus dari album ini"), "playOnTv": MessageLookupByLibrary.simpleMessage("Putar album di TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Langganan PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1001,12 +1020,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Silakan hubungi tim bantuan jika masalah terus terjadi"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Harap berikan izin"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Silakan masuk akun lagi"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Silakan coba lagi"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1040,7 +1059,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Buat tiket dukungan"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Nilai app ini"), "rateUs": MessageLookupByLibrary.simpleMessage("Beri kami nilai"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Pulihkan"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Pulihkan akun"), "recoverButton": MessageLookupByLibrary.simpleMessage("Pulihkan"), @@ -1068,7 +1089,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Berikan kode ini ke teman kamu"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ia perlu daftar ke paket berbayar"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referensi"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Rujukan sedang dijeda"), @@ -1090,7 +1111,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Hapus link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Hapus peserta"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Hapus label orang"), "removePublicLink": @@ -1106,7 +1127,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Ubah nama file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Perpanjang langganan"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "resendEmail": @@ -1157,7 +1178,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album, nama dan jenis file"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Segera tiba: Penelusuran wajah & ajaib ✨"), - "searchResultCount": m113, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Keamanan"), "selectALocation": MessageLookupByLibrary.simpleMessage("Pilih lokasi"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( @@ -1182,8 +1203,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Item terpilih akan dihapus dari semua album dan dipindahkan ke sampah."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Kirim"), "sendEmail": MessageLookupByLibrary.simpleMessage("Kirim email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Kirim undangan"), @@ -1204,16 +1225,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bagikan album sekarang"), "shareLink": MessageLookupByLibrary.simpleMessage("Bagikan link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bagikan hanya dengan orang yang kamu inginkan"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Unduh Ente agar kita bisa berbagi foto dan video kualitas asli dengan mudah\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bagikan ke pengguna non-Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Bagikan album pertamamu"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1226,7 +1247,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Foto terbagi baru"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Terima notifikasi apabila seseorang menambahkan foto ke album bersama yang kamu ikuti"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Dibagikan dengan saya"), "sharedWithYou": @@ -1241,11 +1262,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Keluar di perangkat lain"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Saya menyetujui ketentuan layanan dan kebijakan privasi Ente"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ia akan dihapus dari semua album."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Lewati"), "social": MessageLookupByLibrary.simpleMessage("Sosial"), "someItemsAreInBothEnteAndYourDevice": @@ -1271,6 +1292,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Maaf, kami tidak dapat menghasilkan kunci yang aman di perangkat ini.\n\nHarap mendaftar dengan perangkat lain."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Urut berdasarkan"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Terbaru dulu"), @@ -1287,13 +1310,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Keluarga"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Kamu"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Batas penyimpanan terlampaui"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Kuat"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Berlangganan"), "subscription": MessageLookupByLibrary.simpleMessage("Langganan"), "success": MessageLookupByLibrary.simpleMessage("Berhasil"), @@ -1333,7 +1356,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Item ini akan dihapus dari perangkat ini."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( "Tindakan ini tidak dapat dibatalkan"), "thisAlbumAlreadyHDACollaborativeLink": @@ -1347,7 +1370,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Email ini telah digunakan"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Gambar ini tidak memiliki data exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ini adalah ID Verifikasi kamu"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1413,14 +1436,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gunakan kunci pemulihan"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gunakan foto terpilih"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifikasi gagal, silakan coba lagi"), "verificationId": MessageLookupByLibrary.simpleMessage("ID Verifikasi"), "verify": MessageLookupByLibrary.simpleMessage("Verifikasi"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifikasi email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifikasi passkey"), "verifyPassword": @@ -1450,13 +1473,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Menunggu WiFi..."), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("Kode sumber kami terbuka!"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Lemah"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Selamat datang kembali!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Hal yang baru"), "yearly": MessageLookupByLibrary.simpleMessage("Tahunan"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ya"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ya, batalkan"), "yesConvertToViewer": @@ -1483,7 +1506,7 @@ class MessageLookup extends MessageLookupByLibrary { "Kamu tidak bisa berbagi dengan dirimu sendiri"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Kamu tidak memiliki item di arsip."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Akunmu telah dihapus"), "yourMap": MessageLookupByLibrary.simpleMessage("Peta kamu"), diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 187b4ae653..7a5f154462 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -85,158 +85,160 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} file, ${formattedSize} l\'uno"; - static String m27(newEmail) => "Email cambiata in ${newEmail}"; + static String m28(newEmail) => "Email cambiata in ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} non ha un account Ente."; + + static String m30(email) => "${email} non ha un account Ente.\n\nInvia un invito per condividere foto."; - static String m31(text) => "Trovate foto aggiuntive per ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; + static String m32(text) => "Trovate foto aggiuntive per ${text}"; static String m34(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; - static String m36(endDate) => "La prova gratuita termina il ${endDate}"; + static String m37(endDate) => "La prova gratuita termina il ${endDate}"; - static String m38(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Elaborazione ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementi')}"; - static String m44(email) => + static String m45(email) => "${email} ti ha invitato a essere un contatto fidato"; - static String m45(expiryTime) => "Il link scadrà il ${expiryTime}"; + static String m46(expiryTime) => "Il link scadrà il ${expiryTime}"; - static String m46(email) => "Collega persona a ${email}"; + static String m47(email) => "Collega persona a ${email}"; - static String m50(albumName) => "Spostato con successo su ${albumName}"; + static String m51(albumName) => "Spostato con successo su ${albumName}"; - static String m51(personName) => "Nessun suggerimento per ${personName}"; + static String m52(personName) => "Nessun suggerimento per ${personName}"; - static String m52(name) => "Non è ${name}?"; + static String m53(name) => "Non è ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Per favore contatta ${familyAdminEmail} per cambiare il tuo codice."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Sicurezza password: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Si prega di parlare con il supporto di ${providerName} se ti è stato addebitato qualcosa"; - static String m61(endDate) => + static String m62(endDate) => "Prova gratuita valida fino al ${endDate}.\nIn seguito potrai scegliere un piano a pagamento."; - static String m62(toEmail) => "Per favore invia un\'email a ${toEmail}"; + static String m63(toEmail) => "Per favore invia un\'email a ${toEmail}"; - static String m63(toEmail) => "Invia i log a \n${toEmail}"; + static String m64(toEmail) => "Invia i log a \n${toEmail}"; - static String m65(folderName) => "Elaborando ${folderName}..."; + static String m66(folderName) => "Elaborando ${folderName}..."; - static String m66(storeName) => "Valutaci su ${storeName}"; + static String m67(storeName) => "Valutaci su ${storeName}"; - static String m67(name) => "Riassegnato a ${name}"; + static String m68(name) => "Riassegnato a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Puoi accedere all\'account dopo ${days} giorni. Una notifica verrà inviata a ${email}."; - static String m69(email) => + static String m70(email) => "Ora puoi recuperare l\'account di ${email} impostando una nuova password."; - static String m70(email) => + static String m71(email) => "${email} sta cercando di recuperare il tuo account."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ottenete entrambi ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} verrà rimosso da questo album condiviso\n\nQualsiasi foto aggiunta dall\'utente verrà rimossa dall\'album"; - static String m73(endDate) => "Si rinnova il ${endDate}"; + static String m74(endDate) => "Si rinnova il ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} risultato trovato', other: '${count} risultati trovati')}"; - static String m76(count) => "${count} selezionati"; + static String m78(count) => "${count} selezionati"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} selezionato (${yourCount} tuoi)"; - static String m79(verificationID) => + static String m81(verificationID) => "Ecco il mio ID di verifica: ${verificationID} per ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hey, puoi confermare che questo è il tuo ID di verifica: ${verificationID} su ente.io"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Codice invito Ente: ${referralCode} \n\nInseriscilo in Impostazioni → Generali → Inviti per ottenere ${referralStorageInGB} GB gratis dopo la sottoscrizione a un piano a pagamento\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Condividi con persone specifiche', one: 'Condividi con una persona', other: 'Condividi con ${numberOfPeople} persone')}"; - static String m83(emailIDs) => "Condiviso con ${emailIDs}"; + static String m85(emailIDs) => "Condiviso con ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Questo ${fileType} verrà eliminato dal tuo dispositivo."; - static String m85(fileType) => + static String m87(fileType) => "Questo ${fileType} è sia su Ente che sul tuo dispositivo."; - static String m86(fileType) => "Questo ${fileType} verrà eliminato da Ente."; + static String m88(fileType) => "Questo ${fileType} verrà eliminato da Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} di ${totalAmount} ${totalStorageUnit} utilizzati"; - static String m91(id) => + static String m93(id) => "Il tuo ${id} è già collegato a un altro account Ente.\nSe desideri utilizzare il tuo ${id} con questo account, per favore contatta il nostro supporto\'\'"; - static String m92(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; + static String m94(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} ricordi conservati"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Tocca per caricare, il caricamento è attualmente ignorato a causa di ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Anche loro riceveranno ${storageAmountInGB} GB"; - static String m96(email) => "Questo è l\'ID di verifica di ${email}"; + static String m98(email) => "Questo è l\'ID di verifica di ${email}"; - static String m99(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Presto', one: '1 giorno', other: '${count} giorni')}"; - static String m102(email) => + static String m104(email) => "Sei stato invitato a essere un contatto Legacy da ${email}."; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Il caricamento è ignorato a causa di ${ignoreReason}"; - static String m105(count) => "Conservando ${count} ricordi..."; + static String m107(count) => "Conservando ${count} ricordi..."; - static String m106(endDate) => "Valido fino al ${endDate}"; + static String m108(endDate) => "Valido fino al ${endDate}"; - static String m107(email) => "Verifica ${email}"; + static String m109(email) => "Verifica ${email}"; - static String m109(email) => + static String m112(email) => "Abbiamo inviato una mail a ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} anno fa', other: '${count} anni fa')}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Hai liberato con successo ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -455,6 +457,7 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("Backup dei video"), "birthday": MessageLookupByLibrary.simpleMessage("Compleanno"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Offerta del Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), @@ -526,6 +529,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Clic"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Fai clic sul menu"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Chiudi"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Club per tempo di cattura"), @@ -774,8 +779,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email già registrata."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email non registrata."), "emailVerificationToggle": @@ -857,7 +863,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Trovate foto aggiuntive"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Faccia non ancora raggruppata, per favore torna più tardi"), "faceRecognition": @@ -907,8 +913,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipi di file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipi e nomi di file"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("File eliminati"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File salvati nella galleria"), @@ -924,12 +930,12 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Volti trovati"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spazio gratuito richiesto"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spazio libero utilizzabile"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prova gratuita"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -941,7 +947,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Generali"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generazione delle chiavi di crittografia..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Vai alle impostazioni"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -954,6 +960,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Vista ospite"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Per abilitare la vista ospite, configura il codice di accesso del dispositivo o il blocco schermo nelle impostazioni di sistema."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Non teniamo traccia del numero di installazioni dell\'app. Sarebbe utile se ci dicesse dove ci ha trovato!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1026,12 +1034,14 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Sembra che qualcosa sia andato storto. Riprova tra un po\'. Se l\'errore persiste, contatta il nostro team di supporto."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati saranno rimossi da questo album"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Unirsi a un album renderà visibile la tua email ai suoi partecipanti."), "joinDiscord": MessageLookupByLibrary.simpleMessage("Unisciti a Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Mantieni foto"), @@ -1052,7 +1062,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Account Legacy"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy consente ai contatti fidati di accedere al tuo account in tua assenza."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1067,7 +1077,7 @@ class MessageLookup extends MessageLookupByLibrary { "per una condivisione più veloce"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Attivato"), "linkExpired": MessageLookupByLibrary.simpleMessage("Scaduto"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Scadenza del link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Il link è scaduto"), @@ -1075,12 +1085,12 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Collega persona"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "per una migliore esperienza di condivisione"), - "linkPersonToEmail": m46, + "linkPersonToEmail": m47, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puoi condividere il tuo abbonamento con la tua famiglia"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Fino ad oggi abbiamo conservato oltre 30 milioni di ricordi"), + "Finora abbiamo conservato oltre 200 milioni di ricordi"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1137,6 +1147,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Premi a lungo su un elemento per visualizzarlo a schermo intero"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Loop video disattivo"), "loopVideoOn": @@ -1197,7 +1209,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sposta nell\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Sposta in album nascosto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Spostato nel cestino"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1213,6 +1225,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Nuovo album"), "newLocation": MessageLookupByLibrary.simpleMessage("Nuova posizione"), "newPerson": MessageLookupByLibrary.simpleMessage("Nuova persona"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Prima volta con Ente"), "newest": MessageLookupByLibrary.simpleMessage("Più recenti"), @@ -1250,10 +1263,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nessun risultato"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nessun risultato trovato"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nessun blocco di sistema trovato"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Ancora nulla di condiviso con te"), "nothingToSeeHere": @@ -1263,7 +1276,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Sul dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Su ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo loro"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1302,7 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary { "Password modificata con successo"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blocco con password"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "La sicurezza della password viene calcolata considerando la lunghezza della password, i caratteri usati e se la password appare o meno nelle prime 10.000 password più usate"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1313,7 +1329,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pagamento non riuscito"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Purtroppo il tuo pagamento non è riuscito. Contatta l\'assistenza e ti aiuteremo!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementi in sospeso"), "pendingSync": @@ -1344,7 +1360,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Blocco con PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Riproduci album sulla TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1356,14 +1372,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Riprova. Se il problema persiste, ti invitiamo a contattare l\'assistenza"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Concedi i permessi"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Effettua nuovamente l\'accesso"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Si prega di selezionare i link rapidi da rimuovere"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage( @@ -1388,7 +1404,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Condivisioni private"), "proceed": MessageLookupByLibrary.simpleMessage("Prosegui"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Elaborando video"), "publicLinkCreated": @@ -1401,11 +1417,13 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Invia ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Valuta l\'app"), "rateUs": MessageLookupByLibrary.simpleMessage("Lascia una recensione"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Riassegna \"Io\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Riassegnando..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recupera"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recupera account"), @@ -1414,7 +1432,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recupera l\'account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recupero avviato"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chiave di recupero"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1429,12 +1447,12 @@ class MessageLookup extends MessageLookupByLibrary { "Chiave di recupero verificata"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Se hai dimenticato la password, la tua chiave di ripristino è l\'unico modo per recuperare le tue foto. La puoi trovare in Impostazioni > Account.\n\nInserisci la tua chiave di recupero per verificare di averla salvata correttamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recupero riuscito!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contatto fidato sta tentando di accedere al tuo account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Il dispositivo attuale non è abbastanza potente per verificare la tua password, ma la possiamo rigenerare in un modo che funzioni su tutti i dispositivi.\n\nEffettua il login utilizzando la tua chiave di recupero e rigenera la tua password (puoi utilizzare nuovamente la stessa se vuoi)."), "recreatePasswordTitle": @@ -1450,7 +1468,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Condividi questo codice con i tuoi amici"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Si iscrivono per un piano a pagamento"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Invita un Amico"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "I referral code sono attualmente in pausa"), @@ -1479,7 +1497,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Elimina link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Rimuovi etichetta persona"), "removePublicLink": @@ -1499,7 +1517,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rinomina file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Rinnova abbonamento"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Rinvia email"), @@ -1576,7 +1594,7 @@ class MessageLookup extends MessageLookupByLibrary { "Invita persone e vedrai qui tutte le foto condivise da loro"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Le persone saranno mostrate qui una volta che l\'elaborazione e la sincronizzazione saranno completate"), - "searchResultCount": m113, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Sicurezza"), "selectALocation": MessageLookupByLibrary.simpleMessage("Seleziona un luogo"), @@ -1608,8 +1626,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Invia"), "sendEmail": MessageLookupByLibrary.simpleMessage("Invia email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Invita"), @@ -1641,16 +1659,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Condividi un album"), "shareLink": MessageLookupByLibrary.simpleMessage("Condividi link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Condividi solo con le persone che vuoi"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Scarica Ente in modo da poter facilmente condividere foto e video in qualità originale\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Condividi con utenti che non hanno un account Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Condividi il tuo primo album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1661,7 +1679,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nuove foto condivise"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ricevi notifiche quando qualcuno aggiunge una foto a un album condiviso, di cui fai parte"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Condivisi con me"), "sharedWithYou": @@ -1678,11 +1696,11 @@ class MessageLookup extends MessageLookupByLibrary { "Esci dagli altri dispositivi"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Accetto i termini di servizio e la politica sulla privacy"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Verrà eliminato da tutti gli album."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Salta"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1710,6 +1728,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Siamo spiacenti, non possiamo generare le chiavi sicure su questo dispositivo.\n\nPer favore, accedi da un altro dispositivo."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordina"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordina per"), "sortNewestFirst": @@ -1731,13 +1751,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famiglia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite d\'archiviazione superato"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Iscriviti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "È necessario un abbonamento a pagamento attivo per abilitare la condivisione."), @@ -1754,7 +1774,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggerisci una funzionalità"), "support": MessageLookupByLibrary.simpleMessage("Assistenza"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizzazione interrotta"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1767,7 +1787,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tocca per sbloccare"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Premi per caricare"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Sembra che qualcosa sia andato storto. Riprova tra un po\'. Se l\'errore persiste, contatta il nostro team di supporto."), "terminate": MessageLookupByLibrary.simpleMessage("Terminata"), @@ -1788,7 +1808,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Questi file verranno eliminati dal tuo dispositivo."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Verranno eliminati da tutti gli album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1807,7 +1827,7 @@ class MessageLookup extends MessageLookupByLibrary { "Questa immagine non ha dati EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Questo sono io!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Questo è il tuo ID di verifica"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1831,11 +1851,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totale"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"), "trash": MessageLookupByLibrary.simpleMessage("Cestino"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Taglia"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatti fidati"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Attiva il backup per caricare automaticamente i file aggiunti a questa cartella del dispositivo su Ente."), @@ -1881,10 +1901,10 @@ class MessageLookup extends MessageLookupByLibrary { "Aggiornamento della selezione delle cartelle..."), "upgrade": MessageLookupByLibrary.simpleMessage("Acquista altro spazio"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Caricamento dei file nell\'album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Conservando 1 ricordo..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1903,7 +1923,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usa la foto selezionata"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spazio utilizzato"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifica fallita, per favore prova di nuovo"), @@ -1911,7 +1931,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID di verifica"), "verify": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifica email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifica passkey"), @@ -1951,7 +1971,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Non puoi modificare foto e album che non possiedi"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Debole"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bentornato/a!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Novità"), @@ -1959,7 +1979,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contatto fidato può aiutare a recuperare i tuoi dati."), "yearShort": MessageLookupByLibrary.simpleMessage("anno"), "yearly": MessageLookupByLibrary.simpleMessage("Annuale"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Si"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sì, cancella"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1991,7 +2011,7 @@ class MessageLookup extends MessageLookupByLibrary { "Non puoi condividere con te stesso"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Non hai nulla di archiviato."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Il tuo account è stato eliminato"), "yourMap": MessageLookupByLibrary.simpleMessage("La tua mappa"), diff --git a/mobile/lib/generated/intl/messages_ja.dart b/mobile/lib/generated/intl/messages_ja.dart index afd0ea6616..3aca3277ca 100644 --- a/mobile/lib/generated/intl/messages_ja.dart +++ b/mobile/lib/generated/intl/messages_ja.dart @@ -87,191 +87,191 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} 個のファイル、それぞれ${formattedSize}"; - static String m27(newEmail) => "メールアドレスが ${newEmail} に変更されました"; + static String m28(newEmail) => "メールアドレスが ${newEmail} に変更されました"; - static String m28(email) => "${email} は Ente アカウントを持っていません。"; + static String m29(email) => "${email} は Ente アカウントを持っていません。"; - static String m29(email) => + static String m30(email) => "${email} はEnteアカウントを持っていません。\n\n写真を共有するために「招待」を送信してください。"; - static String m30(name) => "${name}抱きしめて!"; + static String m31(name) => "${name}抱きしめて!"; - static String m31(text) => "${text} の写真が見つかりました"; + static String m32(text) => "${text} の写真が見つかりました"; - static String m32(name) => "${name}とご飯!"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, other: '${formattedNumber} 個のファイル')} が安全にバックアップされました"; + static String m33(name) => "${name}とご飯!"; static String m34(count, formattedNumber) => + "${Intl.plural(count, other: '${formattedNumber} 個のファイル')} が安全にバックアップされました"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} ファイル')} が安全にバックアップされました"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "誰かが有料プランにサインアップしてコードを適用する度に ${storageAmountInGB} GB"; - static String m36(endDate) => "無料トライアルは${endDate} までです"; + static String m37(endDate) => "無料トライアルは${endDate} までです"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; + static String m39(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "${currentlyProcessing} / ${totalCount} を処理中"; - static String m41(name) => "${name}とハイキング!"; + static String m42(name) => "${name}とハイキング!"; - static String m42(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; + static String m43(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; - static String m43(name) => "前回の${name}との時間"; + static String m44(name) => "前回の${name}との時間"; - static String m44(email) => "${email} があなたを信頼する連絡先として招待しました"; + static String m45(email) => "${email} があなたを信頼する連絡先として招待しました"; - static String m45(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; + static String m46(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; - static String m46(email) => "この人物を ${email}に紐づけ"; + static String m47(email) => "この人物を ${email}に紐づけ"; - static String m47(personName, email) => "${personName} を ${email} に紐づけします"; + static String m48(personName, email) => "${personName} を ${email} に紐づけします"; - static String m50(albumName) => "${albumName} に移動しました"; + static String m51(albumName) => "${albumName} に移動しました"; - static String m51(personName) => "${personName} の候補はありません"; + static String m52(personName) => "${personName} の候補はありません"; - static String m52(name) => "${name} ではありませんか?"; + static String m53(name) => "${name} ではありませんか?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "コードを変更するには、 ${familyAdminEmail} までご連絡ください。"; - static String m54(name) => "${name}とパーティー!"; + static String m55(name) => "${name}とパーティー!"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "パスワードの長さ: ${passwordStrengthValue}"; - static String m56(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; + static String m57(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; - static String m57(name, age) => "${name}が${age}才!"; + static String m58(name, age) => "${name}が${age}才!"; - static String m58(name, age) => "${name}が${age}才になった!"; + static String m59(name, age) => "${name}が${age}才になった!"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: '0枚の写真', one: '1枚の写真', other: '${count} 枚の写真')}"; - static String m61(endDate) => + static String m62(endDate) => "${endDate} まで無料トライアルが有効です。\nその後、有料プランを選択することができます。"; - static String m62(toEmail) => "${toEmail} にメールでご連絡ください"; + static String m63(toEmail) => "${toEmail} にメールでご連絡ください"; - static String m63(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; + static String m64(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; - static String m64(name) => "${name}と一緒にポーズ!"; + static String m65(name) => "${name}と一緒にポーズ!"; - static String m65(folderName) => "${folderName} を処理中..."; + static String m66(folderName) => "${folderName} を処理中..."; - static String m66(storeName) => "${storeName} で評価"; + static String m67(storeName) => "${storeName} で評価"; - static String m67(name) => "あなたを ${name} に紐づけました"; + static String m68(name) => "あなたを ${name} に紐づけました"; - static String m68(days, email) => + static String m69(days, email) => "${days} 日後にアカウントにアクセスできます。通知は ${email}に送信されます。"; - static String m69(email) => "${email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。"; + static String m70(email) => "${email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。"; - static String m70(email) => "${email} はあなたのアカウントを復元しようとしています。"; + static String m71(email) => "${email} はあなたのアカウントを復元しようとしています。"; - static String m71(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; + static String m72(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} はこの共有アルバムから退出します\n\n${userEmail} が追加した写真もアルバムから削除されます"; - static String m73(endDate) => "サブスクリプションは ${endDate} に更新します"; + static String m74(endDate) => "サブスクリプションは ${endDate} に更新します"; - static String m74(name) => "${name}と車で旅行!"; + static String m75(name) => "${name}と車で旅行!"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} 個の結果', other: '${count} 個の結果')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "セクションの長さの不一致: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} 個を選択"; + static String m78(count) => "${count} 個を選択"; - static String m77(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; + static String m79(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; - static String m78(name) => "${name}とセルフィー!"; + static String m80(name) => "${name}とセルフィー!"; - static String m79(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; + static String m81(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; - static String m80(verificationID) => + static String m82(verificationID) => "これがあなたのente.io確認用IDであることを確認できますか? ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "リフェラルコード: ${referralCode}\n\n設定→一般→リフェラルで使うことで${referralStorageInGB}が無料になります(あなたが有料プランに加入したあと)。\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '誰かと共有しましょう', one: '1人と共有されています', other: '${numberOfPeople} 人と共有されています')}"; - static String m83(emailIDs) => "${emailIDs} と共有中"; - - static String m84(fileType) => "${fileType} はEnteから削除されます。"; - - static String m85(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; + static String m85(emailIDs) => "${emailIDs} と共有中"; static String m86(fileType) => "${fileType} はEnteから削除されます。"; - static String m87(name) => "${name}とスポーツ!"; + static String m87(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; - static String m88(name) => "${name}にスポットライト!"; + static String m88(fileType) => "${fileType} はEnteから削除されます。"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m89(name) => "${name}とスポーツ!"; - static String m90( + static String m90(name) => "${name}にスポットライト!"; + + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} 使用"; - static String m91(id) => + static String m93(id) => "あなたの ${id} はすでに別のEnteアカウントにリンクされています。\nこのアカウントであなたの ${id} を使用したい場合は、サポートにお問い合わせください。"; - static String m92(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; + static String m94(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; - static String m93(completed, total) => "${completed}/${total} のメモリが保存されました"; + static String m95(completed, total) => "${completed}/${total} のメモリが保存されました"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "アップロードするにはタップしてください。 以下の理由のためアップロードは現在無視されています: ${ignoreReason}"; - static String m95(storageAmountInGB) => "紹介者も ${storageAmountInGB} GB を得ます"; + static String m97(storageAmountInGB) => "紹介者も ${storageAmountInGB} GB を得ます"; - static String m96(email) => "これは ${email} の確認用ID"; - - static String m97(count) => - "${Intl.plural(count, one: '${count} 1年前の今週', other: '${count}年前の今週')}"; - - static String m98(dateFormat) => "${dateFormat} から年"; + static String m98(email) => "これは ${email} の確認用ID"; static String m99(count) => + "${Intl.plural(count, one: '${count} 1年前の今週', other: '${count}年前の今週')}"; + + static String m100(dateFormat) => "${dateFormat} から年"; + + static String m101(count) => "${Intl.plural(count, zero: '', one: '1日', other: '${count} 日')}"; - static String m100(year) => "${year}年の旅行"; + static String m102(year) => "${year}年の旅行"; - static String m101(location) => "${location}への旅行"; + static String m103(location) => "${location}への旅行"; - static String m102(email) => "あなたは ${email}から信頼する連絡先になってもらうよう、お願いされています。"; + static String m104(email) => "あなたは ${email}から信頼する連絡先になってもらうよう、お願いされています。"; - static String m103(galleryType) => + static String m105(galleryType) => "このギャラリーのタイプ ${galleryType} は名前の変更には対応していません"; - static String m104(ignoreReason) => "以下の理由によりアップロードは無視されます: ${ignoreReason}"; + static String m106(ignoreReason) => "以下の理由によりアップロードは無視されます: ${ignoreReason}"; - static String m105(count) => "${count} メモリを保存しています..."; + static String m107(count) => "${count} メモリを保存しています..."; - static String m106(endDate) => "${endDate} まで"; + static String m108(endDate) => "${endDate} まで"; - static String m107(email) => "${email} を確認"; + static String m109(email) => "${email} を確認"; - static String m109(email) => "${email}にメールを送りました"; + static String m112(email) => "${email}にメールを送りました"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m111(name) => "あなたと${name}"; + static String m114(name) => "あなたと${name}"; - static String m112(storageSaved) => "${storageSaved} を解放しました"; + static String m115(storageSaved) => "${storageSaved} を解放しました"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -459,23 +459,9 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("動画をバックアップ"), "beach": MessageLookupByLibrary.simpleMessage("砂浜と海"), "birthday": MessageLookupByLibrary.simpleMessage("誕生日"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("ブラックフライデーセール"), "blog": MessageLookupByLibrary.simpleMessage("ブログ"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("日付を一括編集"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "複数の写真を、1回の操作で日付/時刻を編集することもできるようになりました。"), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage("ファミリープランの制限"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "ファミリーメンバーが使用できるストレージ容量の制限を設定できるようになりました。"), - "cLIcon": MessageLookupByLibrary.simpleMessage("新しいアイコン"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "新しいアプリのアイコンが登場です!ちなみに、古いアイコンが好きだった人のためにアイコンの切り替え機能も用意がございます。"), - "cLMemories": MessageLookupByLibrary.simpleMessage("思い出"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "あなたにとって特別な瞬間を再発見しましょう。 - あなたの好きな人々、あなたの旅行や休日。 機械学習をオンにすると、自分自身にタグを付けたり、友達の顔に名前をつけたりすることができます。"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("ウィジェット"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Enteの「思い出」機能と統合されたホーム画面ウィジェットが利用可能になりました!"), "cachedData": MessageLookupByLibrary.simpleMessage("キャッシュデータ"), "calculating": MessageLookupByLibrary.simpleMessage("計算中..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -537,6 +523,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• クリック"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• 三点ドットをクリックしてください"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("閉じる"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("時間ごとにまとめる"), "clubByFileName": MessageLookupByLibrary.simpleMessage("ファイル名ごとにまとめる"), @@ -739,15 +727,15 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Eメール"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("このメールアドレスはすでに登録されています。"), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("このメールアドレスはまだ登録されていません。"), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("メール確認"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("ログをメールで送信"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("緊急連絡先"), "empty": MessageLookupByLibrary.simpleMessage("空"), "emptyTrash": MessageLookupByLibrary.simpleMessage("ゴミ箱を空にしますか?"), @@ -814,7 +802,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("データをエクスポート"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("追加の写真が見つかりました"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage("顔がまだ集まっていません。後で戻ってきてください"), "faceRecognition": MessageLookupByLibrary.simpleMessage("顔認識"), @@ -847,7 +835,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("よくある質問"), "faqs": MessageLookupByLibrary.simpleMessage("よくある質問"), "favorite": MessageLookupByLibrary.simpleMessage("お気に入り"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("フィードバック"), "file": MessageLookupByLibrary.simpleMessage("ファイル"), "fileFailedToSaveToGallery": @@ -859,8 +847,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ファイルをギャラリーに保存しました"), "fileTypes": MessageLookupByLibrary.simpleMessage("ファイルの種類"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("ファイルの種類と名前"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("削除されたファイル"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("写真をダウンロードしました"), @@ -872,12 +860,12 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("パスワードを忘れた"), "foundFaces": MessageLookupByLibrary.simpleMessage("見つかった顔"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("空き容量を受け取る"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("無料のストレージが利用可能です"), "freeTrial": MessageLookupByLibrary.simpleMessage("無料トライアル"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("デバイスの空き領域を解放する"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -889,7 +877,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("設定"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("暗号化鍵を生成しています"), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("設定に移動"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -901,6 +889,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("ゲストビュー"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "アプリのロックを有効にするには、システム設定でデバイスのパスコードまたは画面ロックを設定してください。"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "私たちはアプリのインストールを追跡していませんが、もしよければ、Enteをお知りになった場所を教えてください!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -916,7 +906,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage("ホームギャラリーから共有された写真等を非表示"), "hiding": MessageLookupByLibrary.simpleMessage("非表示にしています"), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Franceでホスト"), "howItWorks": MessageLookupByLibrary.simpleMessage("仕組みを知る"), @@ -967,7 +957,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("完全に削除されるまでの日数が項目に表示されます"), "itemsWillBeRemovedFromAlbum": @@ -986,7 +976,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("よければ、情報をお寄せください"), "language": MessageLookupByLibrary.simpleMessage("言語"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("更新された順"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("昨年の旅行"), "leave": MessageLookupByLibrary.simpleMessage("離脱"), @@ -997,7 +987,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("左"), "legacy": MessageLookupByLibrary.simpleMessage("レガシー"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("レガシーアカウント"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "レガシーでは、信頼できる連絡先が不在時(あなたが亡くなった時など)にアカウントにアクセスできます。"), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1013,20 +1003,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("共有を高速化するために"), "linkEnabled": MessageLookupByLibrary.simpleMessage("有効"), "linkExpired": MessageLookupByLibrary.simpleMessage("期限切れ"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("リンクの期限切れ"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("リンクは期限切れです"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("なし"), "linkPerson": MessageLookupByLibrary.simpleMessage("人を紐づけ"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("良い経験を分かち合うために"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("ライブフォト"), "loadMessage1": MessageLookupByLibrary.simpleMessage("サブスクリプションを家族と共有できます"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "私たちはこれまでに3000万以上の思い出を保存してきました"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "私たちはあなたのデータのコピーを3つ保管しています。1つは地下のシェルターにあります。"), "loadMessage4": @@ -1077,6 +1065,8 @@ class MessageLookup extends MessageLookupByLibrary { "表示されているEメールアドレスを長押しして、暗号化を確認します。"), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage("アイテムを長押しして全画面表示する"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("ビデオのループをオフ"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("ビデオのループをオン"), "lostDevice": MessageLookupByLibrary.simpleMessage("デバイスを紛失しましたか?"), @@ -1131,7 +1121,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("選択した写真を1つの日付に移動"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("アルバムに移動"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("隠しアルバムに移動"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("ごみ箱へ移動"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルを移動中"), @@ -1145,6 +1135,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("新しいアルバム"), "newLocation": MessageLookupByLibrary.simpleMessage("新しいロケーション"), "newPerson": MessageLookupByLibrary.simpleMessage("新しい人物"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("範囲を追加"), "newToEnte": MessageLookupByLibrary.simpleMessage("Enteを初めて使用する"), "newest": MessageLookupByLibrary.simpleMessage("新しい順"), @@ -1178,10 +1169,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("該当なし"), "noResultsFound": MessageLookupByLibrary.simpleMessage("一致する結果が見つかりませんでした"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("システムロックが見つかりませんでした"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("この人ではありませんか?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("あなたに共有されたものはありません"), @@ -1193,7 +1184,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Enteが保管"), "onTheRoad": MessageLookupByLibrary.simpleMessage("再び道で"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("この人のみ"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": @@ -1221,7 +1215,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairWithPin": MessageLookupByLibrary.simpleMessage("PINを使ってペアリングする"), "pairingComplete": MessageLookupByLibrary.simpleMessage("ペアリング完了"), "panorama": MessageLookupByLibrary.simpleMessage("パノラマ"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("検証はまだ保留中です"), "passkey": MessageLookupByLibrary.simpleMessage("パスキー"), @@ -1230,7 +1224,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("パスワードの変更に成功しました"), "passwordLock": MessageLookupByLibrary.simpleMessage("パスワード保護"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "パスワードの長さ、使用される文字の種類を考慮してパスワードの強度は計算されます。"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1239,7 +1233,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支払いに失敗しました"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "残念ながらお支払いに失敗しました。サポートにお問い合わせください。お手伝いします!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("処理待ちの項目"), "pendingSync": MessageLookupByLibrary.simpleMessage("同期を保留中"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1250,14 +1244,14 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("完全に削除"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("デバイスから完全に削除しますか?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("人名名"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("毛むくじゃらな仲間たち"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("写真の説明"), "photoGridSize": MessageLookupByLibrary.simpleMessage("写真のグリッドサイズ"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("写真"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("写真"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("あなたの追加した写真はこのアルバムから削除されます"), @@ -1268,7 +1262,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PINロック"), "playOnTv": MessageLookupByLibrary.simpleMessage("TVでアルバムを再生"), "playOriginal": MessageLookupByLibrary.simpleMessage("元動画を再生"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("再生"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStoreサブスクリプション"), @@ -1279,13 +1273,13 @@ class MessageLookup extends MessageLookupByLibrary { "Support@ente.ioにお問い合わせください、お手伝いいたします。"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("問題が解決しない場合はサポートにお問い合わせください"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("権限を付与してください"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("削除するクイックリンクを選択してください"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("入力したコードを確認してください"), @@ -1296,7 +1290,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("再試行する前にしばらくお待ちください"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage("しばらくお待ちください。時間がかかります。"), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("ログを準備中..."), "preserveMore": MessageLookupByLibrary.simpleMessage("もっと保存する"), "pressAndHoldToPlayVideo": @@ -1312,7 +1306,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("続行"), "processed": MessageLookupByLibrary.simpleMessage("処理完了"), "processing": MessageLookupByLibrary.simpleMessage("処理中"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("動画を処理中"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公開リンクが作成されました"), @@ -1324,17 +1318,19 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("サポートを受ける"), "rateTheApp": MessageLookupByLibrary.simpleMessage("アプリを評価"), "rateUs": MessageLookupByLibrary.simpleMessage("評価して下さい"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"自分\" を再割り当て"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("再割り当て中..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("復元"), "recoverAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), "recoverButton": MessageLookupByLibrary.simpleMessage("復元"), "recoveryAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("リカバリが開始されました"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキー"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("リカバリーキーはクリップボードにコピーされました"), @@ -1348,12 +1344,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーが確認されました"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "パスワードを忘れた場合、リカバリーキーは写真を復元するための唯一の方法になります。なお、設定 > アカウント でリカバリーキーを確認することができます。\n \n\nここにリカバリーキーを入力して、正しく保存できていることを確認してください。"), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("復元に成功しました!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "信頼する連絡先の持ち主があなたのアカウントにアクセスしようとしています"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "このデバイスではパスワードを確認する能力が足りません。\n\n恐れ入りますが、リカバリーキーを入力してパスワードを再生成する必要があります。"), "recreatePasswordTitle": @@ -1367,7 +1363,7 @@ class MessageLookup extends MessageLookupByLibrary { "referralStep1": MessageLookupByLibrary.simpleMessage("1. このコードを友達に贈りましょう"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 友達が有料プランに登録"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("リフェラル"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("リフェラルは現在一時停止しています"), @@ -1392,7 +1388,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeInvite": MessageLookupByLibrary.simpleMessage("招待を削除"), "removeLink": MessageLookupByLibrary.simpleMessage("リンクを削除"), "removeParticipant": MessageLookupByLibrary.simpleMessage("参加者を削除"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("人名を削除"), "removePublicLink": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), @@ -1409,7 +1405,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("ファイル名を変更"), "renewSubscription": MessageLookupByLibrary.simpleMessage("サブスクリプションの更新"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("バグを報告"), "reportBug": MessageLookupByLibrary.simpleMessage("バグを報告"), "resendEmail": MessageLookupByLibrary.simpleMessage("メールを再送信"), @@ -1429,7 +1425,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("重複だと思うファイルを確認して削除してください"), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("提案を確認"), "right": MessageLookupByLibrary.simpleMessage("右"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("回転"), "rotateLeft": MessageLookupByLibrary.simpleMessage("左に回転"), "rotateRight": MessageLookupByLibrary.simpleMessage("右に回転"), @@ -1476,8 +1472,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("友達を招待すると、共有される写真はここから閲覧できます"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage("処理と同期が完了すると、ここに人々が表示されます"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("セキュリティ"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage("アプリ内で公開アルバムのリンクを見る"), @@ -1517,9 +1513,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "選択したアイテムはこの人としての登録が解除されますが、ライブラリからは削除されません。"), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("送信"), "sendEmail": MessageLookupByLibrary.simpleMessage("メールを送信する"), "sendInvite": MessageLookupByLibrary.simpleMessage("招待を送る"), @@ -1543,16 +1539,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("アルバムを開いて右上のシェアボタンをタップ"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("アルバムを共有"), "shareLink": MessageLookupByLibrary.simpleMessage("リンクの共有"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("選んだ人と共有します"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Enteをダウンロードして、写真や動画の共有を簡単に!\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Enteを使っていない人に共有"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("アルバムの共有をしてみましょう"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1563,7 +1559,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新しい共有写真"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("誰かが写真を共有アルバムに追加した時に通知を受け取る"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("あなたと共有されたアルバム"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("あなたと共有されています"), "sharing": MessageLookupByLibrary.simpleMessage("共有中..."), @@ -1578,11 +1574,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("他のデバイスからサインアウトする"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "利用規約プライバシーポリシーに同意します"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("全てのアルバムから削除されます。"), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("スキップ"), "social": MessageLookupByLibrary.simpleMessage("SNS"), "someItemsAreInBothEnteAndYourDevice": @@ -1608,13 +1604,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "このデバイスでは安全な鍵を生成することができませんでした。\n\n他のデバイスからサインアップを試みてください。"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("並び替え"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("並び替え"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("新しい順"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("古い順"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("成功✨"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("あなた自身にスポットライト!"), "startAccountRecoveryTitle": @@ -1626,14 +1624,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("ストレージ"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("ファミリー"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("あなた"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("ストレージの上限を超えました"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("動画の詳細"), "strongStrength": MessageLookupByLibrary.simpleMessage("強いパスワード"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("サブスクライブ"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "共有を有効にするには、有料サブスクリプションが必要です。"), @@ -1648,7 +1646,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("機能を提案"), "sunrise": MessageLookupByLibrary.simpleMessage("水平線"), "support": MessageLookupByLibrary.simpleMessage("サポート"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("同期が停止しました"), "syncing": MessageLookupByLibrary.simpleMessage("同期中..."), "systemTheme": MessageLookupByLibrary.simpleMessage("システム"), @@ -1656,7 +1654,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToEnterCode": MessageLookupByLibrary.simpleMessage("タップしてコードを入力"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("タップして解除"), "tapToUpload": MessageLookupByLibrary.simpleMessage("タップしてアップロード"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), "terminate": MessageLookupByLibrary.simpleMessage("終了させる"), @@ -1675,7 +1673,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("テーマ"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("これらの項目はデバイスから削除されます。"), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("全てのアルバムから削除されます。"), "thisActionCannotBeUndone": @@ -1692,12 +1690,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("この画像にEXIFデータはありません"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("これは私です"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("これはあなたの認証IDです"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("毎年のこの週"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage("以下のデバイスからログアウトします:"), "thisWillLogYouOutOfThisDevice": @@ -1707,7 +1705,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "選択したすべてのクイックリンクの公開リンクを削除します。"), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "アプリのロックを有効にするには、システム設定でデバイスのパスコードまたは画面ロックを設定してください。"), @@ -1721,12 +1719,12 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("合計"), "totalSize": MessageLookupByLibrary.simpleMessage("合計サイズ"), "trash": MessageLookupByLibrary.simpleMessage("ゴミ箱"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("トリミング"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("信頼する連絡先"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "バックアップをオンにすると、このデバイスフォルダに追加されたファイルは自動的にEnteにアップロードされます。"), @@ -1741,7 +1739,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("2段階認証をリセットしました"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("2段階認証のセットアップ"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("アーカイブ解除"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("アルバムのアーカイブ解除"), "unarchiving": MessageLookupByLibrary.simpleMessage("アーカイブを解除中..."), @@ -1761,10 +1759,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("フォルダの選択を更新しています..."), "upgrade": MessageLookupByLibrary.simpleMessage("アップグレード"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルをアップロード中"), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("1メモリを保存しています..."), "upto50OffUntil4thDec": @@ -1780,13 +1778,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキーを使用"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("選択した写真を使用"), "usedSpace": MessageLookupByLibrary.simpleMessage("使用済み領域"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("確認に失敗しました、再試行してください"), "verificationId": MessageLookupByLibrary.simpleMessage("確認用ID"), "verify": MessageLookupByLibrary.simpleMessage("確認"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Eメールの確認"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("確認"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("パスキーを確認"), "verifyPassword": MessageLookupByLibrary.simpleMessage("パスワードの確認"), @@ -1795,7 +1793,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーを確認中..."), "videoInfo": MessageLookupByLibrary.simpleMessage("ビデオ情報"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("ビデオ"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("動画ストリーミング"), "videos": MessageLookupByLibrary.simpleMessage("ビデオ"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("アクティブなセッションを表示"), @@ -1820,7 +1817,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "あなたが所有していない写真やアルバムの編集はサポートされていません"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("弱いパスワード"), "welcomeBack": MessageLookupByLibrary.simpleMessage("おかえりなさい!"), "whatsNew": MessageLookupByLibrary.simpleMessage("最新情報"), @@ -1828,7 +1825,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("信頼する連絡先は、データの復旧が必要な際に役立ちます。"), "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("年額"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("はい"), "yesCancel": MessageLookupByLibrary.simpleMessage("キャンセル"), "yesConvertToViewer": @@ -1841,7 +1838,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesRenew": MessageLookupByLibrary.simpleMessage("はい、更新する"), "yesResetPerson": MessageLookupByLibrary.simpleMessage("リセット"), "you": MessageLookupByLibrary.simpleMessage("あなた"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("ファミリープランに入会しています!"), "youAreOnTheLatestVersion": @@ -1858,7 +1855,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("自分自身と共有することはできません"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("アーカイブした項目はありません"), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("アカウントは削除されました"), "yourMap": MessageLookupByLibrary.simpleMessage("あなたの地図"), diff --git a/mobile/lib/generated/intl/messages_km.dart b/mobile/lib/generated/intl/messages_km.dart index 22d4231361..0b7524e0cd 100644 --- a/mobile/lib/generated/intl/messages_km.dart +++ b/mobile/lib/generated/intl/messages_km.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'km'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index e378d62fd9..84823c7050 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -26,7 +26,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("다시 오신 것을 환영합니다!"), "askDeleteReason": MessageLookupByLibrary.simpleMessage("계정을 삭제하는 가장 큰 이유가 무엇인가요?"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "cancel": MessageLookupByLibrary.simpleMessage("닫기"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("계정 삭제 확인"), "deleteAccount": MessageLookupByLibrary.simpleMessage("계정 삭제"), @@ -38,8 +41,20 @@ class MessageLookup extends MessageLookupByLibrary { "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("이메일을 입력하세요"), "feedback": MessageLookupByLibrary.simpleMessage("피드백"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("잘못된 이메일 주소"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "verify": MessageLookupByLibrary.simpleMessage("인증"), "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("계정이 삭제되었습니다.") diff --git a/mobile/lib/generated/intl/messages_lt.dart b/mobile/lib/generated/intl/messages_lt.dart index e17d3826ee..86e493031c 100644 --- a/mobile/lib/generated/intl/messages_lt.dart +++ b/mobile/lib/generated/intl/messages_lt.dart @@ -23,16 +23,20 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Aš)"; static String m1(count) => - "${Intl.plural(count, zero: 'Pridėti bendradarbių', one: 'Pridėti bendradarbį', few: 'Pridėti bendradarbius', many: 'Pridėti bendradarbio', other: 'Pridėti bendradarbių')}"; + "${Intl.plural(count, zero: 'Pridėti bendradarbių', one: 'Pridėti bendradarbį', other: 'Pridėti bendradarbių')}"; static String m2(count) => - "${Intl.plural(count, one: 'Pridėti elementą', few: 'Pridėti elementus', many: 'Pridėti elemento', other: 'Pridėti elementų')}"; + "${Intl.plural(count, one: 'Pridėti elementą', other: 'Pridėti elementų')}"; static String m3(storageAmount, endDate) => "Jūsų ${storageAmount} priedas galioja iki ${endDate}"; static String m4(count) => - "${Intl.plural(count, zero: 'Pridėti žiūrėtojų', one: 'Pridėti žiūrėtoją', few: 'Pridėti žiūrėtojus', many: 'Pridėti žiūrėtojo', other: 'Pridėti žiūrėtojų')}"; + "${Intl.plural(count, zero: 'Pridėti žiūrėtojų', one: 'Pridėti žiūrėtoją', other: 'Pridėti žiūrėtojų')}"; + + static String m5(emailOrName) => "Įtraukė ${emailOrName}"; + + static String m6(albumName) => "Sėkmingai įtraukta į „${albumName}“"; static String m7(name) => "Žavisi ${name}"; @@ -41,6 +45,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m9(versionValue) => "Versija: ${versionValue}"; + static String m10(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} laisva"; + static String m11(name) => "Gražūs vaizdai su ${name}"; static String m12(paymentProvider) => @@ -56,11 +63,14 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Jūs gavote ${storageAmountInGb} GB iki šiol.', })}"; + static String m15(albumName) => + "Bendradarbiavimo nuoroda sukurta albumui „${albumName}“"; + static String m16(count) => "${Intl.plural(count, zero: 'Pridėta 0 bendradarbių', one: 'Pridėtas 1 bendradarbis', other: 'Pridėta ${count} bendradarbių')}"; static String m17(email, numOfDays) => - "Ketinate pridėti ${email} kaip patikimą kontaktą. Jie galės atkurti jūsų paskyrą, jei jūsų nebus ${numOfDays} dienų."; + "Ketinate įtraukti ${email} kaip patikimą kontaktą. Jie galės atkurti jūsų paskyrą, jei jūsų nebus ${numOfDays} dienų."; static String m18(familyAdminEmail) => "Susisiekite su ${familyAdminEmail}, kad sutvarkytumėte savo prenumeratą."; @@ -71,7 +81,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Prijungta prie ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Ištrinti ${count} elementą', few: 'Ištrinti ${count} elementus', many: 'Ištrinti ${count} elemento', other: 'Ištrinti ${count} elementų')}"; + "${Intl.plural(count, one: 'Ištrinti ${count} elementą', other: 'Ištrinti ${count} elementų')}"; static String m22(currentlyDeleting, totalCount) => "Ištrinama ${currentlyDeleting} / ${totalCount}"; @@ -83,182 +93,233 @@ class MessageLookup extends MessageLookupByLibrary { "Iš savo registruoto el. pašto adreso atsiųskite el. laišką adresu ${supportEmail}"; static String m25(count, storageSaved) => - "Išvalėte ${Intl.plural(count, one: '${count} dubliuojantį failą', few: '${count} dubliuojančius failus', many: '${count} dubliuojančio failo', other: '${count} dubliuojančių failų')}, išsaugodami (${storageSaved})."; + "Išvalėte ${Intl.plural(count, one: '${count} dubliuojantį failą', other: '${count} dubliuojančių failų')}, išsaugodami (${storageSaved})"; static String m26(count, formattedSize) => "${count} failai (-ų), kiekvienas ${formattedSize}"; - static String m27(newEmail) => "El. paštas pakeistas į ${newEmail}"; + static String m27(name) => "Šis el. paštas jau susietas su ${name}."; - static String m28(email) => "${email} neturi „Ente“ paskyros."; + static String m28(newEmail) => "El. paštas pakeistas į ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} neturi „Ente“ paskyros."; + + static String m30(email) => "${email} neturi „Ente“ paskyros.\n\nSiųskite jiems kvietimą bendrinti nuotraukas."; - static String m31(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; + static String m31(name) => "Apkabinat ${name}"; - static String m35(storageAmountInGB) => + static String m32(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; + + static String m33(name) => "Vaišiavimas su ${name}"; + + static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '${formattedNumber} failas šiame įrenginyje saugiai sukurta atsarginė kopija', few: '${formattedNumber} failai šiame įrenginyje saugiai sukurtos atsarginės kopijos', many: '${formattedNumber} failo šiame įrenginyje saugiai sukurtos atsargines kopijos', other: '${formattedNumber} failų šiame įrenginyje saugiai sukurta atsarginių kopijų')}."; + + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '${formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija', few: '${formattedNumber} failai šiame albume saugiai sukurtos atsarginės kopijos', many: '${formattedNumber} failo šiame albume saugiai sukurtos atsargines kopijos', other: '${formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija')}."; + + static String m36(storageAmountInGB) => "${storageAmountInGB} GB kiekvieną kartą, kai kas nors užsiregistruoja mokamam planui ir pritaiko jūsų kodą."; - static String m36(endDate) => + static String m37(endDate) => "Nemokamas bandomasis laikotarpis galioja iki ${endDate}"; - static String m37(count) => - "Vis dar galite pasiekti ${Intl.plural(count, one: 'jį', few: 'juos', many: 'juos', other: 'jų')} platformoje „Ente“, kol turite aktyvų prenumeratą."; + static String m38(count) => + "Vis dar galite pasiekti ${Intl.plural(count, one: 'jį', other: 'jų')} platformoje „Ente“, kol turite aktyvų prenumeratą."; - static String m38(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Jį galima ištrinti iš įrenginio, kad atlaisvintų ${formattedSize}', other: 'Jų galima ištrinti iš įrenginio, kad atlaisvintų ${formattedSize}')}"; + + static String m41(currentlyProcessing, totalCount) => "Apdorojama ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => - "${Intl.plural(count, one: '${count} elementas', few: '${count} elementai', many: '${count} elemento', other: '${count} elementų')}"; + static String m42(name) => "Žygiavimas su ${name}"; - static String m44(email) => "${email} pakvietė jus būti patikimu kontaktu"; + static String m43(count) => + "${Intl.plural(count, one: '${count} elementas', other: '${count} elementų')}"; - static String m45(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; + static String m44(name) => "Paskutinį kartą su ${name}"; - static String m47(personName, email) => + static String m45(email) => "${email} pakvietė jus būti patikimu kontaktu"; + + static String m46(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; + + static String m47(email) => "Susieti asmenį su ${email}"; + + static String m48(personName, email) => "Tai susies ${personName} su ${email}."; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'nėra prisiminimų', one: '${formattedCount} prisiminimas', few: '${formattedCount} prisiminimai', many: '${formattedCount} prisiminimo', other: '${formattedCount} prisiminimų')}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'nėra prisiminimų', one: '${formattedCount} prisiminimas', other: '${formattedCount} prisiminimų')}"; - static String m49(count) => - "${Intl.plural(count, one: 'Perkelti elementą', few: 'Perkelti elementus', many: 'Perkelti elemento', other: 'Perkelti elementų')}"; + static String m50(count) => + "${Intl.plural(count, one: 'Perkelti elementą', other: 'Perkelti elementų')}"; - static String m51(personName) => "Nėra pasiūlymų asmeniui ${personName}."; + static String m51(albumName) => "Sėkmingai perkelta į „${albumName}“"; - static String m52(name) => "Ne ${name}?"; + static String m52(personName) => "Nėra pasiūlymų asmeniui ${personName}."; - static String m53(familyAdminEmail) => + static String m53(name) => "Ne ${name}?"; + + static String m54(familyAdminEmail) => "Susisiekite su ${familyAdminEmail}, kad pakeistumėte savo kodą."; - static String m55(passwordStrengthValue) => + static String m55(name) => "Vakarėlis su ${name}"; + + static String m56(passwordStrengthValue) => "Slaptažodžio stiprumas: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Kreipkitės į ${providerName} palaikymo komandą, jei jums buvo nuskaičiuota."; - static String m59(count) => - "${Intl.plural(count, zero: 'Nėra nuotraukų', one: '1 nuotrauka', few: '${count} nuotraukos', many: '${count} nuotraukos', other: '${count} nuotraukų')}"; + static String m58(name, age) => "${name} yra ${age} m.!"; + + static String m59(name, age) => "${name} netrukus sulauks ${age} m."; static String m60(count) => + "${Intl.plural(count, zero: 'Nėra nuotraukų', one: '1 nuotrauka', other: '${count} nuotraukų')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 nuotraukų', one: '1 nuotrauka', few: '${count} nuotraukos', many: '${count} nuotraukos', other: '${count} nuotraukų')}"; - static String m61(endDate) => + static String m62(endDate) => "Nemokama bandomoji versija galioja iki ${endDate}.\nVėliau galėsite pasirinkti mokamą planą."; - static String m63(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; + static String m63(toEmail) => "Siųskite el. laišką mums adresu ${toEmail}."; - static String m65(folderName) => "Apdorojama ${folderName}..."; + static String m64(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; - static String m66(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; + static String m65(name) => "Pozavimas su ${name}"; - static String m67(name) => "Perskirstė jus į ${name}"; + static String m66(folderName) => "Apdorojama ${folderName}..."; - static String m68(days, email) => + static String m67(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; + + static String m68(name) => "Perskirstė jus į ${name}"; + + static String m69(days, email) => "Paskyrą galėsite pasiekti po ${days} dienų. Pranešimas bus išsiųstas į ${email}."; - static String m69(email) => + static String m70(email) => "Dabar galite atkurti ${email} paskyrą nustatydami naują slaptažodį."; - static String m70(email) => "${email} bando atkurti jūsų paskyrą."; + static String m71(email) => "${email} bando atkurti jūsų paskyrą."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Abu gaunate ${storageInGB} GB* nemokamai"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} bus pašalintas iš šio bendrinamo albumo.\n\nVisos jų pridėtos nuotraukos taip pat bus pašalintos iš albumo."; - static String m73(endDate) => "Prenumerata pratęsiama ${endDate}"; + static String m74(endDate) => "Prenumerata pratęsiama ${endDate}"; - static String m113(count) => - "${Intl.plural(count, one: 'Rastas ${count} rezultatas', few: 'Rasti ${count} rezultatai', many: 'Rasta ${count} rezultato', other: 'Rasta ${count} rezultatų')}"; + static String m75(name) => "Kelionė su ${name}"; - static String m75(snapshotLength, searchLength) => + static String m76(count) => + "${Intl.plural(count, one: 'Rastas ${count} rezultatas', other: 'Rasta ${count} rezultatų')}"; + + static String m77(snapshotLength, searchLength) => "Sekcijų ilgio neatitikimas: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} pasirinkta"; + static String m78(count) => "${count} pasirinkta"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} pasirinkta (${yourCount} jūsų)"; - static String m79(verificationID) => + static String m80(name) => "Asmenukės su ${name}"; + + static String m81(verificationID) => "Štai mano patvirtinimo ID: ${verificationID}, skirta ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Ei, ar galite patvirtinti, kad tai yra jūsų ente.io patvirtinimo ID: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "„Ente“ rekomendacijos kodas: ${referralCode} \n\nTaikykite jį per Nustatymai → Bendrieji → Rekomendacijos, kad gautumėte ${referralStorageInGB} GB nemokamai po to, kai užsiregistruosite mokamam planui.\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bendrinti su konkrečiais asmenimis', one: 'Bendrinta su 1 asmeniu', other: 'Bendrinta su ${numberOfPeople} asmenimis')}"; - static String m84(fileType) => + static String m85(emailIDs) => "Bendrinta su ${emailIDs}"; + + static String m86(fileType) => "Šis ${fileType} bus ištrintas iš jūsų įrenginio."; - static String m85(fileType) => + static String m87(fileType) => "Šis ${fileType} yra ir saugykloje „Ente“ bei įrenginyje."; - static String m86(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; + static String m88(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m89(name) => "Sportai su ${name}"; - static String m91(id) => + static String m90(name) => "Dėmesys ${name}"; + + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( + usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => + "${usedAmount} ${usedStorageUnit} iš ${totalAmount} ${totalStorageUnit} naudojama"; + + static String m93(id) => "Jūsų ${id} jau susietas su kita „Ente“ paskyra.\nJei norite naudoti savo ${id} su šia paskyra, susisiekite su mūsų palaikymo komanda."; - static String m92(endDate) => "Jūsų prenumerata bus atsisakyta ${endDate}"; + static String m94(endDate) => "Jūsų prenumerata bus atsisakyta ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed} / ${total} išsaugomi prisiminimai"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Palieskite, kad įkeltumėte. Įkėlimas šiuo metu ignoruojamas dėl ${ignoreReason}."; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Jie taip pat gauna ${storageAmountInGB} GB"; - static String m96(email) => "Tai – ${email} patvirtinimo ID"; - - static String m97(count) => - "${Intl.plural(count, one: 'Šią savaitę, prieš ${count} metus', few: 'Šią savaitę, prieš ${count} metus', many: 'Šią savaitę, prieš ${count} metų', other: 'Šią savaitę, prieš ${count} metų')}"; - - static String m98(dateFormat) => "${dateFormat} per metus"; + static String m98(email) => "Tai – ${email} patvirtinimo ID"; static String m99(count) => + "${Intl.plural(count, one: 'Šią savaitę, prieš ${count} metus', other: 'Šią savaitę, prieš ${count} metų')}"; + + static String m100(dateFormat) => "${dateFormat} per metus"; + + static String m101(count) => "${Intl.plural(count, zero: 'Netrukus', one: '1 diena', other: '${count} dienų')}"; - static String m100(year) => "Kelionė per ${year}"; + static String m102(year) => "Kelionė per ${year}"; - static String m101(location) => "Kelionė į ${location}"; + static String m103(location) => "Kelionė į ${location}"; - static String m102(email) => + static String m104(email) => "Buvote pakviesti tapti ${email} palikimo kontaktu."; - static String m103(galleryType) => + static String m105(galleryType) => "Galerijos tipas ${galleryType} nepalaikomas pervadinimui."; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Įkėlimas ignoruojamas dėl ${ignoreReason}."; - static String m106(endDate) => "Galioja iki ${endDate}"; + static String m107(count) => "Išsaugomi ${count} prisiminimai..."; - static String m107(email) => "Patvirtinti ${email}"; + static String m108(endDate) => "Galioja iki ${endDate}"; - static String m108(count) => - "${Intl.plural(count, zero: 'Pridėta 0 žiūrėtojų', one: 'Pridėtas 1 žiūrėtojas', few: 'Pridėti ${count} žiūrėtojai', many: 'Pridėta ${count} žiūrėtojo', other: 'Pridėta ${count} žiūrėtojų')}"; + static String m109(email) => "Patvirtinti ${email}"; - static String m109(email) => + static String m110(name) => "Peržiūrėkite ${name}, kad atsietumėte"; + + static String m111(count) => + "${Intl.plural(count, zero: 'Įtraukta 0 žiūrėtojų', one: 'Įtrauktas 1 žiūrėtojas', few: 'Įtraukti ${count} žiūrėtojai', many: 'Įtraukta ${count} žiūrėtojo', other: 'Įtraukta ${count} žiūrėtojų')}"; + + static String m112(email) => "Išsiuntėme laišką adresu ${email}"; - static String m110(count) => - "${Intl.plural(count, one: 'prieš ${count} metus', few: 'prieš ${count} metus', many: 'prieš ${count} metų', other: 'prieš ${count} metų')}"; + static String m113(count) => + "${Intl.plural(count, one: 'prieš ${count} metus', other: 'prieš ${count} metų')}"; - static String m111(name) => "Jūs ir ${name}"; + static String m114(name) => "Jūs ir ${name}"; - static String m112(storageSaved) => "Sėkmingai atlaisvinote ${storageSaved}!"; + static String m115(storageSaved) => "Sėkmingai atlaisvinote ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -280,11 +341,13 @@ class MessageLookup extends MessageLookupByLibrary { "add": MessageLookupByLibrary.simpleMessage("Pridėti"), "addAName": MessageLookupByLibrary.simpleMessage("Pridėti vardą"), "addANewEmail": - MessageLookupByLibrary.simpleMessage("Pridėti naują el. paštą"), + MessageLookupByLibrary.simpleMessage("Įtraukite naują el. paštą"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Pridėti bendradarbį"), "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Pridėti failus"), + "addFromDevice": + MessageLookupByLibrary.simpleMessage("Pridėti iš įrenginio"), "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Pridėti vietovę"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Pridėti"), @@ -299,13 +362,22 @@ class MessageLookup extends MessageLookupByLibrary { "Išsami informacija apie priedus"), "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Priedai"), + "addPhotos": MessageLookupByLibrary.simpleMessage("Įtraukti nuotraukų"), + "addSelected": + MessageLookupByLibrary.simpleMessage("Pridėti pasirinktus"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Pridėti į albumą"), "addToEnte": MessageLookupByLibrary.simpleMessage("Pridėti į „Ente“"), + "addToHiddenAlbum": + MessageLookupByLibrary.simpleMessage("Įtraukti į paslėptą albumą"), "addTrustedContact": MessageLookupByLibrary.simpleMessage("Pridėti patikimą kontaktą"), "addViewer": MessageLookupByLibrary.simpleMessage("Pridėti žiūrėtoją"), "addViewers": m4, + "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( + "Įtraukite savo nuotraukas dabar"), "addedAs": MessageLookupByLibrary.simpleMessage("Pridėta kaip"), + "addedBy": m5, + "addedSuccessfullyTo": m6, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Pridedama prie mėgstamų..."), "admiringThem": m7, @@ -348,8 +420,12 @@ class MessageLookup extends MessageLookupByLibrary { "Leisti prieigą prie nuotraukų"), "androidBiometricHint": MessageLookupByLibrary.simpleMessage("Patvirtinkite tapatybę"), + "androidBiometricNotRecognized": MessageLookupByLibrary.simpleMessage( + "Neatpažinta. Bandykite dar kartą."), "androidBiometricRequiredTitle": MessageLookupByLibrary.simpleMessage("Privaloma biometrija"), + "androidBiometricSuccess": + MessageLookupByLibrary.simpleMessage("Sėkmė"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("Atšaukti"), "androidDeviceCredentialsRequiredTitle": MessageLookupByLibrary.simpleMessage( @@ -398,6 +474,8 @@ class MessageLookup extends MessageLookupByLibrary { "Jūsų prenumerata buvo atšaukta. Ar norėtumėte pasidalyti priežastimi?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Kokia yra pagrindinė priežastis, dėl kurios ištrinate savo paskyrą?"), + "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage( + "Paprašykite savo artimuosius bendrinti"), "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("priešgaisrinėje slėptuvėje"), "authToChangeEmailVerificationSetting": @@ -424,6 +502,8 @@ class MessageLookup extends MessageLookupByLibrary { "Nustatykite tapatybę, kad peržiūrėtumėte savo aktyvius seansus"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( "Nustatykite tapatybę, kad peržiūrėtumėte paslėptus failus"), + "authToViewYourMemories": MessageLookupByLibrary.simpleMessage( + "Nustatykite tapatybę, kad peržiūrėtumėte savo prisiminimus"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Nustatykite tapatybę, kad peržiūrėtumėte savo atkūrimo raktą"), "authenticating": @@ -435,6 +515,8 @@ class MessageLookup extends MessageLookupByLibrary { "Tapatybės nustatymas sėkmingas."), "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( "Čia matysite pasiekiamus perdavimo įrenginius."), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "Įsitikinkite, kad programai „Ente“ nuotraukos yra įjungti vietinio tinklo leidimai, nustatymuose."), "autoLock": MessageLookupByLibrary.simpleMessage("Automatinis užraktas"), "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( @@ -446,11 +528,14 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatinis susiejimas veikia tik su įrenginiais, kurie palaiko „Chromecast“."), "available": MessageLookupByLibrary.simpleMessage("Prieinama"), + "availableStorageSpace": m10, "backedUpFolders": MessageLookupByLibrary.simpleMessage( "Sukurtos atsarginės aplankų kopijos"), "backgroundWithThem": m11, "backup": MessageLookupByLibrary.simpleMessage("Kurti atsarginę kopiją"), + "backupFailed": + MessageLookupByLibrary.simpleMessage("Atsarginė kopija nepavyko"), "backupFile": MessageLookupByLibrary.simpleMessage( "Kurti atsarginę failo kopiją"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage( @@ -465,32 +550,23 @@ class MessageLookup extends MessageLookupByLibrary { "Kurti atsargines vaizdo įrašų kopijas"), "beach": MessageLookupByLibrary.simpleMessage("Smėlis ir jūra"), "birthday": MessageLookupByLibrary.simpleMessage("Gimtadienis"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Juodojo penktadienio išpardavimas"), "blog": MessageLookupByLibrary.simpleMessage("Tinklaraštis"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Masiškai redaguokite datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galite pasirinkti kelias nuotraukas ir vienu sparčiu veiksmu redaguoti visų nuotraukų datą ir laiką. Taip pat palaikomas datų perkėlimas."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Šeimos plano ribos"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galite nustatyti ribas, kiek saugyklos gali naudoti jūsų šeimos nariai."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nauja piktograma"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Pagaliau – nauja programos piktograma, kuri, mūsų manymu, geriausiai atspindi mūsų kūrybą. Taip pat pridėjome piktogramos perjungiklį, tad galite ir toliau naudoti senąją piktogramą."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Prisiminimai"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Iš naujo atraskite ypatingas akimirkas – atkreipkite dėmesį į mėgstamus asmenis, keliones ir atostogas, geriausias nuotraukas bei daug daugiau. Įjunkite mašininį mokymąsi, pažymėkite save ir įvardykite draugus dėl geriausios patirties."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Valdikliai"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Dabar galima naudoti su prisiminimais integruotus pagrindinio ekrano valdiklius. Jie parodys jūsų ypatingas akimirkas neatvėrus programos."), "cachedData": MessageLookupByLibrary.simpleMessage("Podėliuoti duomenis"), + "calculating": MessageLookupByLibrary.simpleMessage("Skaičiuojama..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( "Atsiprašome, šio albumo negalima atverti programoje."), "canNotOpenTitle": MessageLookupByLibrary.simpleMessage("Negalima atverti šio albumo"), + "canNotUploadToAlbumsOwnedByOthers": + MessageLookupByLibrary.simpleMessage( + "Negalima įkelti į kitiems priklausančius albumus"), + "canOnlyCreateLinkForFilesOwnedByYou": + MessageLookupByLibrary.simpleMessage( + "Galima sukurti nuorodą tik jums priklausantiems failams"), "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Galima pašalinti tik jums priklausančius failus"), "cancel": MessageLookupByLibrary.simpleMessage("Atšaukti"), @@ -511,7 +587,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nepavyko perduoti albumo"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Aplankykite cast.ente.io įrenginyje, kurį norite susieti.\n\nĮveskite toliau esantį kodą, kad paleistumėte albumą televizoriuje."), - "centerPoint": MessageLookupByLibrary.simpleMessage("Vidurio taškas"), + "centerPoint": MessageLookupByLibrary.simpleMessage("Centro taškas"), "change": MessageLookupByLibrary.simpleMessage("Keisti"), "changeEmail": MessageLookupByLibrary.simpleMessage("Keisti el. paštą"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( @@ -545,9 +621,16 @@ class MessageLookup extends MessageLookupByLibrary { "clearCaches": MessageLookupByLibrary.simpleMessage("Valyti podėlius"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Valyti indeksavimus"), + "click": MessageLookupByLibrary.simpleMessage("• Spauskite"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Spustelėkite ant perpildymo meniu"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Uždaryti"), + "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( + "Grupuoti pagal užfiksavimo laiką"), + "clubByFileName": MessageLookupByLibrary.simpleMessage( + "Grupuoti pagal failo pavadinimą"), "clusteringProgress": MessageLookupByLibrary.simpleMessage("Sankaupos vykdymas"), "codeAppliedPageTitle": @@ -562,11 +645,15 @@ class MessageLookup extends MessageLookupByLibrary { "Sukurkite nuorodą, kad asmenys galėtų pridėti ir peržiūrėti nuotraukas bendrinamame albume, nereikalaujant „Ente“ programos ar paskyros. Puikiai tinka įvykių nuotraukoms rinkti."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Bendradarbiavimo nuoroda"), + "collaborativeLinkCreatedFor": m15, "collaborator": MessageLookupByLibrary.simpleMessage("Bendradarbis"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Bendradarbiai gali pridėti nuotraukų ir vaizdo įrašų į bendrintą albumą."), "collaboratorsSuccessfullyAdded": m16, + "collageLayout": MessageLookupByLibrary.simpleMessage("Išdėstymas"), + "collageSaved": MessageLookupByLibrary.simpleMessage( + "Koliažas išsaugotas į galeriją"), "collect": MessageLookupByLibrary.simpleMessage("Rinkti"), "collectEventPhotos": MessageLookupByLibrary.simpleMessage("Rinkti įvykių nuotraukas"), @@ -599,9 +686,12 @@ class MessageLookup extends MessageLookupByLibrary { "Susisiekti su palaikymo komanda"), "contactToManageSubscription": m19, "contacts": MessageLookupByLibrary.simpleMessage("Kontaktai"), + "contents": MessageLookupByLibrary.simpleMessage("Turinys"), "continueLabel": MessageLookupByLibrary.simpleMessage("Tęsti"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( "Tęsti nemokame bandomajame laikotarpyje"), + "convertToAlbum": + MessageLookupByLibrary.simpleMessage("Konvertuoti į albumą"), "copyEmailAddress": MessageLookupByLibrary.simpleMessage("Kopijuoti el. pašto adresą"), "copyLink": MessageLookupByLibrary.simpleMessage("Kopijuoti nuorodą"), @@ -614,6 +704,9 @@ class MessageLookup extends MessageLookupByLibrary { "Nepavyko atlaisvinti vietos."), "couldNotUpdateSubscription": MessageLookupByLibrary.simpleMessage( "Nepavyko atnaujinti prenumeratos"), + "count": MessageLookupByLibrary.simpleMessage("Skaičių"), + "crashReporting": + MessageLookupByLibrary.simpleMessage("Pranešti apie strigčius"), "create": MessageLookupByLibrary.simpleMessage("Kurti"), "createAccount": MessageLookupByLibrary.simpleMessage("Kurti paskyrą"), "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( @@ -646,6 +739,8 @@ class MessageLookup extends MessageLookupByLibrary { "declineTrustInvite": MessageLookupByLibrary.simpleMessage("Atmesti kvietimą"), "decrypting": MessageLookupByLibrary.simpleMessage("Iššifruojama..."), + "decryptingVideo": MessageLookupByLibrary.simpleMessage( + "Iššifruojamas vaizdo įrašas..."), "deduplicateFiles": MessageLookupByLibrary.simpleMessage("Atdubliuoti failus"), "delete": MessageLookupByLibrary.simpleMessage("Ištrinti"), @@ -705,6 +800,8 @@ class MessageLookup extends MessageLookupByLibrary { "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( "Ar tikrai norite modifikuoti kūrėjo nustatymus?"), "deviceCodeHint": MessageLookupByLibrary.simpleMessage("Įveskite kodą"), + "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( + "Į šį įrenginio albumą įtraukti failai bus automatiškai įkelti į „Ente“."), "deviceLock": MessageLookupByLibrary.simpleMessage("Įrenginio užraktas"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( @@ -745,10 +842,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Lankymo kortelės"), "discover_wallpapers": MessageLookupByLibrary.simpleMessage("Ekrano fonai"), + "dismiss": MessageLookupByLibrary.simpleMessage("Atmesti"), "distanceInKMUnit": MessageLookupByLibrary.simpleMessage("km"), "doNotSignOut": MessageLookupByLibrary.simpleMessage("Neatsijungti"), "doThisLater": MessageLookupByLibrary.simpleMessage("Daryti tai vėliau"), + "doYouWantToDiscardTheEditsYouHaveMade": + MessageLookupByLibrary.simpleMessage( + "Ar norite atmesti atliktus pakeitimus?"), "done": MessageLookupByLibrary.simpleMessage("Atlikta"), "dontSave": MessageLookupByLibrary.simpleMessage("Neišsaugoti"), "doubleYourStorage": @@ -761,12 +862,15 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Redaguoti"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Redaguoti vietovę"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Redaguoti vietovę"), "editPerson": MessageLookupByLibrary.simpleMessage("Redaguoti asmenį"), "editTime": MessageLookupByLibrary.simpleMessage("Redaguoti laiką"), + "editsSaved": + MessageLookupByLibrary.simpleMessage("Redagavimai išsaugoti"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( "Vietovės pakeitimai bus matomi tik per „Ente“"), @@ -774,15 +878,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("El. paštas"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "El. paštas jau užregistruotas."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("El. paštas neregistruotas."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("El. pašto patvirtinimas"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Atsiųskite žurnalus el. laišku"), + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Skubios pagalbos kontaktai"), "empty": MessageLookupByLibrary.simpleMessage("Ištuštinti"), @@ -807,6 +912,9 @@ class MessageLookup extends MessageLookupByLibrary { "Galutinis taškas sėkmingai atnaujintas"), "endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage( "Pagal numatytąjį užšifruota visapusiškai"), + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": + MessageLookupByLibrary.simpleMessage( + "„Ente“ gali užšifruoti ir išsaugoti failus tik tada, jei suteikiate prieigą prie jų."), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "„Ente“ reikia leidimo išsaugoti jūsų nuotraukas"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( @@ -822,6 +930,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gimtadienis (neprivaloma)"), "enterEmail": MessageLookupByLibrary.simpleMessage("Įveskite el. paštą"), + "enterFileName": + MessageLookupByLibrary.simpleMessage("Įveskite failo pavadinimą"), "enterName": MessageLookupByLibrary.simpleMessage("Įveskite vardą"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Įveskite naują slaptažodį, kurį galime naudoti jūsų duomenims šifruoti"), @@ -841,6 +951,8 @@ class MessageLookup extends MessageLookupByLibrary { "Įveskite tinkamą el. pašto adresą."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Įveskite savo el. pašto adresą"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Įveskite savo naują el. pašto adresą"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Įveskite savo slaptažodį"), "enterYourRecoveryKey": @@ -858,7 +970,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuoti duomenis"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Rastos papildomos nuotraukos"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Veidas dar nesugrupuotas. Grįžkite vėliau."), "faceRecognition": @@ -869,6 +981,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nepavyko pritaikyti kodo."), "failedToCancel": MessageLookupByLibrary.simpleMessage("Nepavyko atsisakyti"), + "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage( + "Nepavyko atsisiųsti vaizdo įrašo."), "failedToFetchActiveSessions": MessageLookupByLibrary.simpleMessage( "Nepavyko gauti aktyvių seansų."), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage( @@ -893,6 +1007,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("DUK"), "faqs": MessageLookupByLibrary.simpleMessage("DUK"), "favorite": MessageLookupByLibrary.simpleMessage("Pamėgti"), + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Atsiliepimai"), "file": MessageLookupByLibrary.simpleMessage("Failas"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -904,12 +1019,18 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage( "Failas išsaugotas į galeriją"), "fileTypes": MessageLookupByLibrary.simpleMessage("Failų tipai"), + "fileTypesAndNames": + MessageLookupByLibrary.simpleMessage("Failų tipai ir pavadinimai"), + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, + "filesDeleted": MessageLookupByLibrary.simpleMessage("Failai ištrinti"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Failai išsaugoti į galeriją"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( "Greitai suraskite žmones pagal vardą"), "findThemQuickly": MessageLookupByLibrary.simpleMessage("Raskite juos greitai"), + "flip": MessageLookupByLibrary.simpleMessage("Apversti"), "food": MessageLookupByLibrary.simpleMessage("Kulinarinis malonumas"), "forYourMemories": MessageLookupByLibrary.simpleMessage("jūsų prisiminimams"), @@ -918,37 +1039,44 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Rasti veidai"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gauta nemokama saugykla"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Naudojama nemokama saugykla"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Nemokamas bandomasis laikotarpis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Atlaisvinti įrenginio vietą"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Sutaupykite vietos savo įrenginyje išvalydami failus, kurių atsarginės kopijos jau buvo sukurtos."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Atlaisvinti vietos"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerija"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Galerijoje rodoma iki 1000 prisiminimų"), "general": MessageLookupByLibrary.simpleMessage("Bendrieji"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generuojami šifravimo raktai..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Eiti į nustatymus"), "googlePlayId": MessageLookupByLibrary.simpleMessage("„Google Play“ ID"), + "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( + "Leiskite prieigą prie visų nuotraukų nustatymų programoje."), "grantPermission": MessageLookupByLibrary.simpleMessage("Suteikti leidimą"), "greenery": MessageLookupByLibrary.simpleMessage("Žaliasis gyvenimas"), + "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage( + "Grupuoti netoliese nuotraukas"), "guestView": MessageLookupByLibrary.simpleMessage("Svečio peržiūra"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte svečio peržiūrą, sistemos nustatymuose nustatykite įrenginio prieigos kodą arba ekrano užraktą."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Mes nesekame programų diegimų. Mums padėtų, jei pasakytumėte, kur mus radote."), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -964,6 +1092,9 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Slėpti bendrinamus elementus iš pagrindinės galerijos"), "hiding": MessageLookupByLibrary.simpleMessage("Slepiama..."), + "hikingWithThem": m42, + "hostedAtOsmFrance": + MessageLookupByLibrary.simpleMessage("Talpinama OSM Prancūzijoje"), "howItWorks": MessageLookupByLibrary.simpleMessage("Kaip tai veikia"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( "Paprašykite jų ilgai paspausti savo el. pašto adresą nustatymų ekrane ir patvirtinti, kad abiejų įrenginių ID sutampa."), @@ -974,6 +1105,8 @@ class MessageLookup extends MessageLookupByLibrary { "iOSOkButton": MessageLookupByLibrary.simpleMessage("Gerai"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignoruoti"), "ignored": MessageLookupByLibrary.simpleMessage("ignoruota"), + "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( + "Kai kurie šio albumo failai ignoruojami, nes anksčiau buvo ištrinti iš „Ente“."), "imageNotAnalyzed": MessageLookupByLibrary.simpleMessage("Vaizdas neanalizuotas."), "immediately": MessageLookupByLibrary.simpleMessage("Iš karto"), @@ -1008,14 +1141,17 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Netinkamas raktas."), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "Įvestas atkūrimo raktas yra netinkamas. Įsitikinkite, kad jame yra 24 žodžiai, ir patikrinkite kiekvieno iš jų rašybą.\n\nJei įvedėte senesnį atkūrimo kodą, įsitikinkite, kad jis yra 64 simbolių ilgio, ir patikrinkite kiekvieną iš jų."), + "invite": MessageLookupByLibrary.simpleMessage("Kviesti"), "inviteToEnte": MessageLookupByLibrary.simpleMessage("Kviesti į „Ente“"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("Kviesti savo draugus"), + "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( + "Pakvieskite savo draugus į „Ente“"), "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Atrodo, kad kažkas nutiko ne taip. Bandykite pakartotinai po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementai rodo likusių dienų skaičių iki visiško ištrynimo."), @@ -1038,6 +1174,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Maloniai padėkite mums su šia informacija."), "language": MessageLookupByLibrary.simpleMessage("Kalba"), + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Paskutinį kartą atnaujintą"), "lastYearsTrip": @@ -1051,7 +1188,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Palikimas"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Palikimo paskyros"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Palikimas leidžia patikimiems kontaktams pasiekti jūsų paskyrą jums nesant."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1059,12 +1196,16 @@ class MessageLookup extends MessageLookupByLibrary { "light": MessageLookupByLibrary.simpleMessage("Šviesi"), "lightTheme": MessageLookupByLibrary.simpleMessage("Šviesi"), "link": MessageLookupByLibrary.simpleMessage("Susieti"), + "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( + "Nuoroda nukopijuota į iškarpinę"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Įrenginių riba"), "linkEmail": MessageLookupByLibrary.simpleMessage("Susieti el. paštą"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("spartesniam bendrinimui"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Įjungta"), "linkExpired": MessageLookupByLibrary.simpleMessage("Nebegalioja"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Nuorodos galiojimo laikas"), "linkHasExpired": @@ -1072,13 +1213,14 @@ class MessageLookup extends MessageLookupByLibrary { "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Niekada"), "linkPerson": MessageLookupByLibrary.simpleMessage("Susiekite asmenį,"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( - "kad geriau bendrintumėte patirtį"), - "linkPersonToEmailConfirmation": m47, + "geresniam bendrinimo patirčiai"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Gyvos nuotraukos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Galite bendrinti savo prenumeratą su šeima."), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Iki šiol išsaugojome daugiau kaip 30 milijonų prisiminimų."), + "Iki šiol išsaugojome daugiau nei 200 milijonų prisiminimų."), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Laikome 3 jūsų duomenų kopijas, vieną iš jų – požeminėje priešgaisrinėje slėptuvėje."), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1093,8 +1235,12 @@ class MessageLookup extends MessageLookupByLibrary { "web.ente.io turi sklandų įkėlėją"), "loadMessage9": MessageLookupByLibrary.simpleMessage( "Naudojame „Xchacha20Poly1305“, kad saugiai užšifruotume jūsų duomenis."), + "loadingExifData": + MessageLookupByLibrary.simpleMessage("Įkeliami EXIF duomenys..."), "loadingGallery": MessageLookupByLibrary.simpleMessage("Įkeliama galerija..."), + "loadingMessage": MessageLookupByLibrary.simpleMessage( + "Įkeliamos jūsų nuotraukos..."), "loadingModel": MessageLookupByLibrary.simpleMessage("Atsisiunčiami modeliai..."), "loadingYourPhotos": @@ -1129,6 +1275,11 @@ class MessageLookup extends MessageLookupByLibrary { "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( "Ilgai paspauskite el. paštą, kad patvirtintumėte visapusį šifravimą."), + "longpressOnAnItemToViewInFullscreen": + MessageLookupByLibrary.simpleMessage( + "Ilgai paspauskite elementą, kad peržiūrėtumėte per visą ekraną"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage( "Išjungtas vaizdo įrašo ciklas"), "loopVideoOn": MessageLookupByLibrary.simpleMessage( @@ -1157,7 +1308,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("„Mastodon“"), "matrix": MessageLookupByLibrary.simpleMessage("„Matrix“"), "me": MessageLookupByLibrary.simpleMessage("Aš"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Atributika"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Sujungti su esamais"), @@ -1178,6 +1329,9 @@ class MessageLookup extends MessageLookupByLibrary { "mobileWebDesktop": MessageLookupByLibrary.simpleMessage( "Mobiliuosiuose, internete ir darbalaukyje"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Vidutinė"), + "modifyYourQueryOrTrySearchingFor": + MessageLookupByLibrary.simpleMessage( + "Modifikuokite užklausą arba bandykite ieškoti"), "moments": MessageLookupByLibrary.simpleMessage("Akimirkos"), "month": MessageLookupByLibrary.simpleMessage("mėnesis"), "monthly": MessageLookupByLibrary.simpleMessage("Mėnesinis"), @@ -1187,13 +1341,18 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Naujausią"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Aktualiausią"), "mountains": MessageLookupByLibrary.simpleMessage("Per kalvas"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Perkelti pasirinktas nuotraukas į vieną datą"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Perkelti į albumą"), + "moveToHiddenAlbum": + MessageLookupByLibrary.simpleMessage("Perkelti į paslėptą albumą"), + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Perkelta į šiukšlinę"), + "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( + "Perkeliami failai į albumą..."), "name": MessageLookupByLibrary.simpleMessage("Pavadinimą"), "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Pavadinkite albumą"), @@ -1205,12 +1364,15 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Naujas albumas"), "newLocation": MessageLookupByLibrary.simpleMessage("Nauja vietovė"), "newPerson": MessageLookupByLibrary.simpleMessage("Naujas asmuo"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Naujas intervalas"), "newToEnte": MessageLookupByLibrary.simpleMessage("Naujas platformoje „Ente“"), "newest": MessageLookupByLibrary.simpleMessage("Naujausią"), "next": MessageLookupByLibrary.simpleMessage("Toliau"), "no": MessageLookupByLibrary.simpleMessage("Ne"), + "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( + "Dar nėra albumų, kuriais bendrinotės."), "noDeviceFound": MessageLookupByLibrary.simpleMessage("Įrenginys nerastas"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Jokio"), @@ -1222,6 +1384,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nėra „Ente“ paskyros!"), "noExifData": MessageLookupByLibrary.simpleMessage("Nėra EXIF duomenų"), "noFacesFound": MessageLookupByLibrary.simpleMessage("Nerasta veidų."), + "noHiddenPhotosOrVideos": MessageLookupByLibrary.simpleMessage( + "Nėra paslėptų nuotraukų arba vaizdo įrašų"), "noImagesWithLocation": MessageLookupByLibrary.simpleMessage("Nėra vaizdų su vietove"), "noInternetConnection": @@ -1229,6 +1393,8 @@ class MessageLookup extends MessageLookupByLibrary { "noPhotosAreBeingBackedUpRightNow": MessageLookupByLibrary.simpleMessage( "Šiuo metu nekuriamos atsarginės nuotraukų kopijos"), + "noPhotosFoundHere": + MessageLookupByLibrary.simpleMessage("Nuotraukų čia nerasta"), "noQuickLinksSelected": MessageLookupByLibrary.simpleMessage( "Nėra pasirinktų sparčiųjų nuorodų"), "noRecoveryKey": @@ -1238,10 +1404,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Rezultatų nėra."), "noResultsFound": MessageLookupByLibrary.simpleMessage("Rezultatų nerasta."), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Nerastas sistemos užraktas"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Ne šis asmuo?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Kol kas su jumis niekuo nesibendrinama."), @@ -1253,9 +1419,14 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Saugykloje ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Vėl kelyje"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Tik jiems"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), + "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( + "Ups, nepavyko išsaugoti redagavimų."), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Ups, kažkas nutiko ne taip"), "openAlbumInBrowser": @@ -1263,8 +1434,12 @@ class MessageLookup extends MessageLookupByLibrary { "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( "Naudokite interneto programą, kad pridėtumėte nuotraukų į šį albumą."), "openFile": MessageLookupByLibrary.simpleMessage("Atverti failą"), + "openSettings": + MessageLookupByLibrary.simpleMessage("Atverti nustatymus"), "openTheItem": MessageLookupByLibrary.simpleMessage("• Atverkite elementą."), + "openstreetmapContributors": MessageLookupByLibrary.simpleMessage( + "„OpenStreetMap“ bendradarbiai"), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( "Nebūtina, trumpai, kaip jums patinka..."), "orMergeWithExistingPerson": @@ -1278,6 +1453,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Susiejimas baigtas"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Vis dar laukiama patvirtinimo"), "passkey": MessageLookupByLibrary.simpleMessage("Slaptaraktis"), @@ -1288,7 +1464,7 @@ class MessageLookup extends MessageLookupByLibrary { "Slaptažodis sėkmingai pakeistas"), "passwordLock": MessageLookupByLibrary.simpleMessage("Slaptažodžio užraktas"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Slaptažodžio stiprumas apskaičiuojamas atsižvelgiant į slaptažodžio ilgį, naudotus simbolius ir į tai, ar slaptažodis patenka į 10 000 dažniausiai naudojamų slaptažodžių."), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1299,7 +1475,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Mokėjimas nepavyko"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Deja, jūsų mokėjimas nepavyko. Susisiekite su palaikymo komanda ir mes jums padėsime!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Laukiami elementai"), "pendingSync": @@ -1307,30 +1483,39 @@ class MessageLookup extends MessageLookupByLibrary { "people": MessageLookupByLibrary.simpleMessage("Asmenys"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( "Asmenys, naudojantys jūsų kodą"), + "permDeleteWarning": MessageLookupByLibrary.simpleMessage( + "Visi elementai šiukšlinėje bus negrįžtamai ištrinti.\n\nŠio veiksmo negalima anuliuoti."), "permanentlyDelete": MessageLookupByLibrary.simpleMessage("Ištrinti negrįžtamai"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Ištrinti negrįžtamai iš įrenginio?"), + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Asmens vardas"), + "personTurningAge": m59, + "pets": MessageLookupByLibrary.simpleMessage("Furio draugai"), + "photoDescriptions": + MessageLookupByLibrary.simpleMessage("Nuotraukų aprašai"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Nuotraukų tinklelio dydis"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("nuotrauka"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Nuotraukos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Jūsų pridėtos nuotraukos bus pašalintos iš albumo"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Nuotraukos išlaiko santykinį laiko skirtumą"), + "pickCenterPoint": + MessageLookupByLibrary.simpleMessage("Pasirinkite centro tašką"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Prisegti albumą"), "pinLock": MessageLookupByLibrary.simpleMessage("PIN užrakinimas"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Paleisti albumą televizoriuje"), "playOriginal": MessageLookupByLibrary.simpleMessage("Leisti originalą"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Leisti srautinį perdavimą"), "playstoreSubscription": @@ -1341,23 +1526,30 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportAndWeWillBeHappyToHelp": MessageLookupByLibrary.simpleMessage( "Susisiekite adresu support@ente.io ir mes mielai padėsime!"), + "pleaseContactSupportIfTheProblemPersists": + MessageLookupByLibrary.simpleMessage( + "Jei problema išlieka, susisiekite su pagalbos komanda."), + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Suteikite leidimus."), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Prisijunkite iš naujo."), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Pasirinkite sparčiąsias nuorodas, kad pašalintumėte"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bandykite dar kartą."), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("Patvirtinkite įvestą kodą."), "pleaseWait": MessageLookupByLibrary.simpleMessage("Palaukite..."), + "pleaseWaitDeletingAlbum": MessageLookupByLibrary.simpleMessage( + "Palaukite. Ištrinamas albumas"), "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( "Palaukite kurį laiką prieš bandydami pakartotinai"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Palaukite, tai šiek tiek užtruks."), + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Ruošiami žurnalai..."), "preserveMore": @@ -1377,17 +1569,26 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Tęsti"), "processed": MessageLookupByLibrary.simpleMessage("Apdorota"), "processing": MessageLookupByLibrary.simpleMessage("Apdorojama"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Apdorojami vaizdo įrašai"), + "publicLinkCreated": + MessageLookupByLibrary.simpleMessage("Vieša nuoroda sukurta"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Įjungta viešoji nuoroda"), "queued": MessageLookupByLibrary.simpleMessage("Įtraukta eilėje"), + "quickLinks": MessageLookupByLibrary.simpleMessage("Sparčios nuorodos"), + "radius": MessageLookupByLibrary.simpleMessage("Spindulys"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Sukurti paraišką"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Vertinti programą"), "rateUs": MessageLookupByLibrary.simpleMessage("Vertinti mus"), - "rateUsOnStore": m66, - "reassignedToName": m67, + "rateUsOnStore": m67, + "reassignMe": MessageLookupByLibrary.simpleMessage("Perskirstyti „Aš“"), + "reassignedToName": m68, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Perskirstoma..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Atkurti"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), @@ -1396,7 +1597,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Pradėtas atkūrimas"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Atkūrimo raktas"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Nukopijuotas atkūrimo raktas į iškarpinę"), @@ -1410,12 +1611,12 @@ class MessageLookup extends MessageLookupByLibrary { "Patvirtintas atkūrimo raktas"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Atkūrimo raktas – vienintelis būdas atkurti nuotraukas, jei pamiršote slaptažodį. Atkūrimo raktą galite rasti Nustatymose > Paskyra.\n\nĮveskite savo atkūrimo raktą čia, kad patvirtintumėte, ar teisingai jį išsaugojote."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Atkūrimas sėkmingas."), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Patikimas kontaktas bando pasiekti jūsų paskyrą."), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Dabartinis įrenginys nėra pakankamai galingas, kad patvirtintų jūsų slaptažodį, bet mes galime iš naujo sugeneruoti taip, kad jis veiktų su visais įrenginiais.\n\nPrisijunkite naudojant atkūrimo raktą ir sugeneruokite iš naujo slaptažodį (jei norite, galite vėl naudoti tą patį)."), "recreatePasswordTitle": @@ -1431,7 +1632,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Duokite šį kodą savo draugams"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Jie užsiregistruoja mokamą planą"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Rekomendacijos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Šiuo metu rekomendacijos yra pristabdytos"), @@ -1463,7 +1664,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Šalinti nuorodą"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Šalinti dalyvį"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Šalinti asmens žymą"), "removePublicLink": @@ -1479,18 +1680,23 @@ class MessageLookup extends MessageLookupByLibrary { "removingFromFavorites": MessageLookupByLibrary.simpleMessage("Pašalinama iš mėgstamų..."), "rename": MessageLookupByLibrary.simpleMessage("Pervadinti"), + "renameAlbum": + MessageLookupByLibrary.simpleMessage("Pervadinti albumą"), "renameFile": MessageLookupByLibrary.simpleMessage("Pervadinti failą"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Pratęsti prenumeratą"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Pranešti apie riktą"), "reportBug": MessageLookupByLibrary.simpleMessage("Pranešti apie riktą"), "resendEmail": MessageLookupByLibrary.simpleMessage("Iš naujo siųsti el. laišką"), + "resetIgnoredFiles": + MessageLookupByLibrary.simpleMessage("Atkurti ignoruojamus failus"), "resetPasswordTitle": MessageLookupByLibrary.simpleMessage( "Nustatyti slaptažodį iš naujo"), + "resetPerson": MessageLookupByLibrary.simpleMessage("Šalinti"), "resetToDefault": MessageLookupByLibrary.simpleMessage( "Atkurti numatytąsias reikšmes"), "restore": MessageLookupByLibrary.simpleMessage("Atkurti"), @@ -1498,24 +1704,33 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atkurti į albumą"), "restoringFiles": MessageLookupByLibrary.simpleMessage("Atkuriami failai..."), + "resumableUploads": + MessageLookupByLibrary.simpleMessage("Tęstiniai įkėlimai"), "retry": MessageLookupByLibrary.simpleMessage("Kartoti"), + "review": MessageLookupByLibrary.simpleMessage("Peržiūrėti"), "reviewDeduplicateItems": MessageLookupByLibrary.simpleMessage( "Peržiūrėkite ir ištrinkite elementus, kurie, jūsų manymu, yra dublikatai."), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Peržiūrėti pasiūlymus"), "right": MessageLookupByLibrary.simpleMessage("Dešinė"), + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Sukti"), + "rotateLeft": MessageLookupByLibrary.simpleMessage("Sukti į kairę"), + "rotateRight": MessageLookupByLibrary.simpleMessage("Sukti į dešinę"), "safelyStored": MessageLookupByLibrary.simpleMessage("Saugiai saugoma"), + "save": MessageLookupByLibrary.simpleMessage("Išsaugoti"), "saveChangesBeforeLeavingQuestion": MessageLookupByLibrary.simpleMessage( "Išsaugoti pakeitimus prieš išeinant?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Išsaugoti koliažą"), + "saveCopy": MessageLookupByLibrary.simpleMessage("Išsaugoti kopiją"), "saveKey": MessageLookupByLibrary.simpleMessage("Išsaugoti raktą"), "savePerson": MessageLookupByLibrary.simpleMessage("Išsaugoti asmenį"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( "Išsaugokite atkūrimo raktą, jei dar to nepadarėte"), + "saving": MessageLookupByLibrary.simpleMessage("Išsaugoma..."), "savingEdits": MessageLookupByLibrary.simpleMessage("Išsaugomi redagavimai..."), "scanCode": MessageLookupByLibrary.simpleMessage("Skenuoti kodą"), @@ -1523,14 +1738,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Skenuokite šį QR kodą\nsu autentifikatoriaus programa"), "search": MessageLookupByLibrary.simpleMessage("Ieškokite"), + "searchAlbumsEmptySection": + MessageLookupByLibrary.simpleMessage("Albumai"), "searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("Albumo pavadinimas"), "searchByExamples": MessageLookupByLibrary.simpleMessage( "• Albumų pavadinimai (pvz., „Fotoaparatas“)\n• Failų tipai (pvz., „Vaizdo įrašai“, „.gif“)\n• Metai ir mėnesiai (pvz., „2022“, „sausis“)\n• Šventės (pvz., „Kalėdos“)\n• Nuotraukų aprašymai (pvz., „#džiaugsmas“)"), "searchCaptionEmptySection": MessageLookupByLibrary.simpleMessage( "Pridėkite aprašymus, pavyzdžiui, „#kelionė“, į nuotraukos informaciją, kad greičiau jas čia rastumėte."), + "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( + "Ieškokite pagal datą, mėnesį arba metus"), "searchDiscoverEmptySection": MessageLookupByLibrary.simpleMessage( "Vaizdai bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas."), + "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( + "Asmenys bus rodomi čia, kai bus užbaigtas indeksavimas."), + "searchFileTypesAndNamesEmptySection": + MessageLookupByLibrary.simpleMessage("Failų tipai ir pavadinimai"), + "searchHint1": + MessageLookupByLibrary.simpleMessage("Sparti paieška įrenginyje"), + "searchHint2": + MessageLookupByLibrary.simpleMessage("Nuotraukų datos ir aprašai"), "searchHint3": MessageLookupByLibrary.simpleMessage( "Albumai, failų pavadinimai ir tipai"), "searchHint4": MessageLookupByLibrary.simpleMessage("Vietovė"), @@ -1542,8 +1769,8 @@ class MessageLookup extends MessageLookupByLibrary { "Pakvieskite asmenis ir čia matysite visas jų bendrinamas nuotraukas."), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Asmenys bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas."), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Saugumas"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Žiūrėti viešų albumų nuorodas programoje"), @@ -1551,6 +1778,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pasirinkite vietovę"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( "Pirmiausia pasirinkite vietovę"), + "selectAlbum": + MessageLookupByLibrary.simpleMessage("Pasirinkti albumą"), "selectAll": MessageLookupByLibrary.simpleMessage("Pasirinkti viską"), "selectAllShort": MessageLookupByLibrary.simpleMessage("Viskas"), "selectCoverPhoto": MessageLookupByLibrary.simpleMessage( @@ -1558,19 +1787,27 @@ class MessageLookup extends MessageLookupByLibrary { "selectDate": MessageLookupByLibrary.simpleMessage("Pasirinkti datą"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( "Pasirinkite aplankus atsarginėms kopijoms kurti"), + "selectItemsToAdd": MessageLookupByLibrary.simpleMessage( + "Pasirinkite elementus įtraukti"), "selectLanguage": MessageLookupByLibrary.simpleMessage("Pasirinkite kalbą"), "selectMailApp": MessageLookupByLibrary.simpleMessage("Pasirinkti pašto programą"), + "selectMorePhotos": MessageLookupByLibrary.simpleMessage( + "Pasirinkti daugiau nuotraukų"), "selectOneDateAndTime": MessageLookupByLibrary.simpleMessage( "Pasirinkti vieną datą ir laiką"), "selectOneDateAndTimeForAll": MessageLookupByLibrary.simpleMessage( "Pasirinkti vieną datą ir laiką viskam"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Pasirinkite asmenį, kurį susieti."), "selectReason": MessageLookupByLibrary.simpleMessage("Pasirinkite priežastį"), "selectStartOfRange": MessageLookupByLibrary.simpleMessage( "Pasirinkti intervalo pradžią"), "selectTime": MessageLookupByLibrary.simpleMessage("Pasirinkti laiką"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Pasirinkite savo veidą"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Pasirinkite planą"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( @@ -1578,11 +1815,15 @@ class MessageLookup extends MessageLookupByLibrary { "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "Pasirinkti aplankai bus užšifruoti ir sukurtos atsarginės kopijos."), + "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": + MessageLookupByLibrary.simpleMessage( + "Pasirinkti elementai bus ištrinti iš visų albumų ir perkelti į šiukšlinę."), "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Pasirinkti elementai bus pašalinti iš šio asmens, bet nebus ištrinti iš jūsų bibliotekos."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Siųsti"), "sendEmail": MessageLookupByLibrary.simpleMessage("Siųsti el. laišką"), "sendInvite": MessageLookupByLibrary.simpleMessage("Siųsti kvietimą"), @@ -1604,6 +1845,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nustatykite naują PIN"), "setPasswordTitle": MessageLookupByLibrary.simpleMessage("Nustatyti slaptažodį"), + "setRadius": MessageLookupByLibrary.simpleMessage("Nustatyti spindulį"), "setupComplete": MessageLookupByLibrary.simpleMessage("Sąranka baigta"), "share": MessageLookupByLibrary.simpleMessage("Bendrinti"), "shareALink": @@ -1613,24 +1855,28 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bendrinti albumą dabar"), "shareLink": MessageLookupByLibrary.simpleMessage("Bendrinti nuorodą"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bendrinkite tik su tais asmenimis, su kuriais norite"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Atsisiųskite „Ente“, kad galėtume lengvai bendrinti originalios kokybės nuotraukas ir vaizdo įrašus.\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bendrinkite su ne „Ente“ naudotojais."), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, + "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( + "Bendrinkite savo pirmąjį albumą"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Sukurkite bendrinamus ir bendradarbiaujamus albumus su kitais „Ente“ naudotojais, įskaitant naudotojus nemokamuose planuose."), + "sharedByMe": MessageLookupByLibrary.simpleMessage("Bendrinta manimi"), "sharedByYou": MessageLookupByLibrary.simpleMessage("Bendrinta iš jūsų"), "sharedPhotoNotifications": MessageLookupByLibrary.simpleMessage( "Naujos bendrintos nuotraukos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( - "Gaukite pranešimus, kai kas nors prideda nuotrauką į bendrinamą albumą, kuriame dalyvaujate."), + "Gaukite pranešimus, kai kas nors įtraukia nuotrauką į bendrinamą albumą, kuriame dalyvaujate."), + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Bendrinta su manimi"), "sharedWithYou": @@ -1641,17 +1887,23 @@ class MessageLookup extends MessageLookupByLibrary { "showMemories": MessageLookupByLibrary.simpleMessage("Rodyti prisiminimus"), "showPerson": MessageLookupByLibrary.simpleMessage("Rodyti asmenį"), + "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage( + "Atsijungti iš kitų įrenginių"), "signOutOtherBody": MessageLookupByLibrary.simpleMessage( "Jei manote, kad kas nors gali žinoti jūsų slaptažodį, galite priverstinai atsijungti iš visų kitų įrenginių, naudojančių jūsų paskyrą."), + "signOutOtherDevices": + MessageLookupByLibrary.simpleMessage("Atsijungti kitus įrenginius"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Sutinku su paslaugų sąlygomis ir privatumo politika"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Jis bus ištrintas iš visų albumų."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Praleisti"), "social": MessageLookupByLibrary.simpleMessage("Socialinės"), + "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( + "Kai kurie elementai yra ir platformoje „Ente“ bei jūsų įrenginyje."), "someOfTheFilesYouAreTryingToDeleteAre": MessageLookupByLibrary.simpleMessage( "Kai kurie failai, kuriuos bandote ištrinti, yra pasiekiami tik jūsų įrenginyje ir jų negalima atkurti, jei jie buvo ištrinti."), @@ -1664,14 +1916,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Kažkas nutiko ne taip. Bandykite dar kartą."), "sorry": MessageLookupByLibrary.simpleMessage("Atsiprašome"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Atsiprašome, šiuo metu negalėjome sukurti atsarginės šio failo kopijos. Bandysime pakartoti vėliau."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Atsiprašome, nepavyko pridėti prie mėgstamų."), "sorryCouldNotRemoveFromFavorites": MessageLookupByLibrary.simpleMessage( "Atsiprašome, nepavyko pašalinti iš mėgstamų."), + "sorryTheCodeYouveEnteredIsIncorrect": + MessageLookupByLibrary.simpleMessage( + "Atsiprašome, įvestas kodas yra neteisingas."), "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Atsiprašome, šiame įrenginyje nepavyko sugeneruoti saugių raktų.\n\nRegistruokitės iš kito įrenginio."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Rikiuoti"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Rikiuoti pagal"), "sortNewestFirst": @@ -1679,6 +1938,10 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Seniausią pirmiausiai"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Sėkmė"), + "sportsWithThem": m89, + "spotlightOnThem": m90, + "spotlightOnYourself": + MessageLookupByLibrary.simpleMessage("Dėmesys į save"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("Pradėti atkūrimą"), "startBackup": MessageLookupByLibrary.simpleMessage( @@ -1689,15 +1952,17 @@ class MessageLookup extends MessageLookupByLibrary { "stopCastingTitle": MessageLookupByLibrary.simpleMessage("Stabdyti perdavimą"), "storage": MessageLookupByLibrary.simpleMessage("Saugykla"), + "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Šeima"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jūs"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Viršyta saugyklos riba."), + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage( "Srautinio perdavimo išsami informacija"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stipri"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Prenumeruoti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte bendrinimą, reikia aktyvios mokamos prenumeratos."), @@ -1705,13 +1970,17 @@ class MessageLookup extends MessageLookupByLibrary { "success": MessageLookupByLibrary.simpleMessage("Sėkmė"), "successfullyArchived": MessageLookupByLibrary.simpleMessage("Sėkmingai suarchyvuota"), + "successfullyHid": + MessageLookupByLibrary.simpleMessage("Sėkmingai paslėptas"), "successfullyUnarchived": MessageLookupByLibrary.simpleMessage("Sėkmingai išarchyvuota"), + "successfullyUnhid": + MessageLookupByLibrary.simpleMessage("Sėkmingai atslėptas"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("Siūlyti funkcijas"), "sunrise": MessageLookupByLibrary.simpleMessage("Akiratyje"), "support": MessageLookupByLibrary.simpleMessage("Pagalba"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage( "Sinchronizavimas sustabdytas"), "syncing": MessageLookupByLibrary.simpleMessage("Sinchronizuojama..."), @@ -1724,7 +1993,7 @@ class MessageLookup extends MessageLookupByLibrary { "Palieskite, kad atrakintumėte"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Palieskite, kad įkeltumėte"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Atrodo, kad kažkas nutiko ne taip. Bandykite dar kartą po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda."), "terminate": MessageLookupByLibrary.simpleMessage("Baigti"), @@ -1744,7 +2013,17 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Įvestas atkūrimo raktas yra neteisingas."), "theme": MessageLookupByLibrary.simpleMessage("Tema"), - "theyAlsoGetXGb": m95, + "theseItemsWillBeDeletedFromYourDevice": + MessageLookupByLibrary.simpleMessage( + "Šie elementai bus ištrinti iš jūsų įrenginio."), + "theyAlsoGetXGb": m97, + "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( + "Jie bus ištrinti iš visų albumų."), + "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( + "Šio veiksmo negalima anuliuoti."), + "thisAlbumAlreadyHDACollaborativeLink": + MessageLookupByLibrary.simpleMessage( + "Šis albumas jau turi bendradarbiavimo nuorodą."), "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Tai gali būti naudojama paskyrai atkurti, jei prarandate dvigubo tapatybės nustatymą"), @@ -1754,12 +2033,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Šis vaizdas neturi Exif duomenų"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Tai aš!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Tai – jūsų patvirtinimo ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Ši savaitė per metus"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Tai jus atjungs nuo toliau nurodyto įrenginio:"), @@ -1771,7 +2050,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Tai pašalins visų pasirinktų sparčiųjų nuorodų viešąsias nuorodas."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte programos užraktą, sistemos nustatymuose nustatykite įrenginio prieigos kodą arba ekrano užraktą."), @@ -1784,20 +2063,26 @@ class MessageLookup extends MessageLookupByLibrary { "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage( "Per daug neteisingų bandymų."), "total": MessageLookupByLibrary.simpleMessage("iš viso"), + "totalSize": MessageLookupByLibrary.simpleMessage("Bendrą dydį"), "trash": MessageLookupByLibrary.simpleMessage("Šiukšlinė"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Trumpinti"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Patikimi kontaktai"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Bandyti dar kartą"), + "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( + "Įjunkite atsarginės kopijos kūrimą, kad automatiškai įkeltumėte į šį įrenginio aplanką įtrauktus failus į „Ente“."), "twitter": MessageLookupByLibrary.simpleMessage("„Twitter“"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 mėnesiai nemokamai metiniuose planuose"), "twofactor": MessageLookupByLibrary.simpleMessage( "Dvigubas tapatybės nustatymas"), + "twofactorAuthenticationHasBeenDisabled": + MessageLookupByLibrary.simpleMessage( + "Dvigubas tapatybės nustatymas išjungtas."), "twofactorAuthenticationPageTitle": MessageLookupByLibrary.simpleMessage( "Dvigubas tapatybės nustatymas"), @@ -1806,7 +2091,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas."), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Dvigubo tapatybės nustatymo sąranka"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Išarchyvuoti"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Išarchyvuoti albumą"), @@ -1817,6 +2102,11 @@ class MessageLookup extends MessageLookupByLibrary { "uncategorized": MessageLookupByLibrary.simpleMessage("Nekategorizuoti"), "unhide": MessageLookupByLibrary.simpleMessage("Rodyti"), + "unhideToAlbum": + MessageLookupByLibrary.simpleMessage("Rodyti į albumą"), + "unhiding": MessageLookupByLibrary.simpleMessage("Rodoma..."), + "unhidingFilesToAlbum": + MessageLookupByLibrary.simpleMessage("Rodomi failai į albumą"), "unlock": MessageLookupByLibrary.simpleMessage("Atrakinti"), "unpinAlbum": MessageLookupByLibrary.simpleMessage("Atsegti albumą"), "unselectAll": MessageLookupByLibrary.simpleMessage("Nesirinkti visų"), @@ -1826,7 +2116,12 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atnaujinamas aplankų pasirinkimas..."), "upgrade": MessageLookupByLibrary.simpleMessage("Keisti planą"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, + "uploadingFilesToAlbum": + MessageLookupByLibrary.simpleMessage("Įkeliami failai į albumą..."), + "uploadingMultipleMemories": m107, + "uploadingSingleMemory": + MessageLookupByLibrary.simpleMessage("Išsaugomas prisiminimas..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( "Iki 50% nuolaida, gruodžio 4 d."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( @@ -1839,8 +2134,10 @@ class MessageLookup extends MessageLookupByLibrary { "Naudokite viešas nuorodas asmenimis, kurie nėra sistemoje „Ente“"), "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Naudoti atkūrimo raktą"), + "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( + "Naudoti pasirinktą nuotrauką"), "usedSpace": MessageLookupByLibrary.simpleMessage("Naudojama vieta"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Patvirtinimas nepavyko. Bandykite dar kartą."), @@ -1849,7 +2146,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Patvirtinti el. paštą"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Patvirtinti slaptaraktį"), @@ -1861,8 +2158,8 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Vaizdo įrašo informacija"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vaizdo įrašas"), - "videoStreaming": MessageLookupByLibrary.simpleMessage( - "Vaizdo įrašų srautinis perdavimas"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Srautiniai vaizdo įrašai"), "videos": MessageLookupByLibrary.simpleMessage("Vaizdo įrašai"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Peržiūrėti aktyvius seansus"), @@ -1875,10 +2172,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Peržiūrėkite failus, kurie užima daugiausiai saugyklos vietos."), "viewLogs": MessageLookupByLibrary.simpleMessage("Peržiūrėti žurnalus"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Peržiūrėti atkūrimo raktą"), "viewer": MessageLookupByLibrary.simpleMessage("Žiūrėtojas"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aplankykite web.ente.io, kad tvarkytumėte savo prenumeratą"), "waitingForVerification": @@ -1891,7 +2189,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nepalaikome nuotraukų ir albumų redagavimo, kurių dar neturite."), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Silpna"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Sveiki sugrįžę!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Kas naujo"), @@ -1899,32 +2197,40 @@ class MessageLookup extends MessageLookupByLibrary { "Patikimas kontaktas gali padėti atkurti jūsų duomenis."), "yearShort": MessageLookupByLibrary.simpleMessage("m."), "yearly": MessageLookupByLibrary.simpleMessage("Metinis"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Taip"), "yesCancel": MessageLookupByLibrary.simpleMessage("Taip, atsisakyti"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("Taip, keisti į žiūrėtoją"), "yesDelete": MessageLookupByLibrary.simpleMessage("Taip, ištrinti"), + "yesDiscardChanges": + MessageLookupByLibrary.simpleMessage("Taip, atmesti pakeitimus"), "yesLogout": MessageLookupByLibrary.simpleMessage("Taip, atsijungti"), "yesRemove": MessageLookupByLibrary.simpleMessage("Taip, šalinti"), "yesRenew": MessageLookupByLibrary.simpleMessage("Taip, pratęsti"), "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Taip, nustatyti asmenį iš naujo"), "you": MessageLookupByLibrary.simpleMessage("Jūs"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Esate šeimos plane!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage("Esate naujausioje versijoje"), "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( "* Galite daugiausiai padvigubinti savo saugyklą."), + "youCanManageYourLinksInTheShareTab": + MessageLookupByLibrary.simpleMessage( + "Nuorodas galite valdyti bendrinimo kortelėje."), + "youCanTrySearchingForADifferentQuery": + MessageLookupByLibrary.simpleMessage( + "Galite pabandyti ieškoti pagal kitą užklausą."), "youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage( "Negalite pakeisti į šį planą"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( "Negalite bendrinti su savimi."), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Neturite jokių archyvuotų elementų."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Jūsų paskyra ištrinta"), "yourMap": MessageLookupByLibrary.simpleMessage("Jūsų žemėlapis"), @@ -1947,6 +2253,11 @@ class MessageLookup extends MessageLookupByLibrary { "Jūsų patvirtinimo kodas nebegaliojantis."), "youveNoDuplicateFilesThatCanBeCleared": MessageLookupByLibrary.simpleMessage( - "Neturite dubliuotų failų, kuriuos būtų galima išvalyti.") + "Neturite dubliuotų failų, kuriuos būtų galima išvalyti."), + "youveNoFilesInThisAlbumThatCanBeDeleted": + MessageLookupByLibrary.simpleMessage( + "Neturite šiame albume failų, kuriuos būtų galima ištrinti."), + "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage( + "Padidinkite mastelį, kad matytumėte nuotraukas") }; } diff --git a/mobile/lib/generated/intl/messages_lv.dart b/mobile/lib/generated/intl/messages_lv.dart index a4ef58ad94..6bd80f8044 100644 --- a/mobile/lib/generated/intl/messages_lv.dart +++ b/mobile/lib/generated/intl/messages_lv.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'lv'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_ml.dart b/mobile/lib/generated/intl/messages_ml.dart index 6a3eec447c..2843561229 100644 --- a/mobile/lib/generated/intl/messages_ml.dart +++ b/mobile/lib/generated/intl/messages_ml.dart @@ -28,10 +28,13 @@ class MessageLookup extends MessageLookupByLibrary { "askDeleteReason": MessageLookupByLibrary.simpleMessage( "അക്കൗണ്ട് ഉപേക്ഷിക്കുവാൻ പ്രധാന കാരണമെന്താണ്?"), "available": MessageLookupByLibrary.simpleMessage("ലഭ്യമാണ്"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "calculating": MessageLookupByLibrary.simpleMessage("കണക്കുകൂട്ടുന്നു..."), "cancel": MessageLookupByLibrary.simpleMessage("റദ്ദാക്കുക"), "changeEmail": MessageLookupByLibrary.simpleMessage("ഇമെയിൽ മാറ്റുക"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("അടക്കുക"), "confirm": MessageLookupByLibrary.simpleMessage("നിജപ്പെടുത്തുക"), "confirmPassword": @@ -68,6 +71,8 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("സങ്കേതക്കുറി മറന്നുപോയി"), "general": MessageLookupByLibrary.simpleMessage("പൊതുവായവ"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hide": MessageLookupByLibrary.simpleMessage("മറയ്ക്കുക"), "howItWorks": MessageLookupByLibrary.simpleMessage("പ്രവർത്തന രീതി"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("അവഗണിക്കുക"), @@ -79,16 +84,22 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("വിവരങ്ങൾ തന്നു സഹായിക്കുക"), "lightTheme": MessageLookupByLibrary.simpleMessage("തെളിഞ"), "linkExpired": MessageLookupByLibrary.simpleMessage("കാലഹരണപ്പെട്ടു"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "mastodon": MessageLookupByLibrary.simpleMessage("മാസ്റ്റഡോൺ"), "matrix": MessageLookupByLibrary.simpleMessage("മേട്രിക്സ്"), "moderateStrength": MessageLookupByLibrary.simpleMessage("ഇടത്തരം"), "monthly": MessageLookupByLibrary.simpleMessage("പ്രതിമാസം"), "name": MessageLookupByLibrary.simpleMessage("പേര്"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "no": MessageLookupByLibrary.simpleMessage("വേണ്ട"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("ഒന്നുമില്ല"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage("ഇവിടൊന്നും കാണ്മാനില്ല! 👀"), "ok": MessageLookupByLibrary.simpleMessage("ശരി"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("അയ്യോ"), "password": MessageLookupByLibrary.simpleMessage("സങ്കേതക്കുറി"), "pleaseTryAgain": @@ -96,6 +107,8 @@ class MessageLookup extends MessageLookupByLibrary { "privacy": MessageLookupByLibrary.simpleMessage("സ്വകാര്യത"), "privacyPolicyTitle": MessageLookupByLibrary.simpleMessage("സ്വകാര്യതാനയം"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recoverButton": MessageLookupByLibrary.simpleMessage("വീണ്ടെടുക്കുക"), "recoverySuccessful": MessageLookupByLibrary.simpleMessage("വീണ്ടെടുക്കൽ വിജയകരം!"), @@ -118,6 +131,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "എന്തോ കുഴപ്പം സംഭവിച്ചു, ദയവായി വീണ്ടും ശ്രമിക്കുക"), "sorry": MessageLookupByLibrary.simpleMessage("ക്ഷമിക്കുക"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("ഇപ്രകാരം അടുക്കുക"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ സഫലം"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index 86615863bd..66b7b60f19 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -22,9 +22,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Ik)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Samenwerker toevoegen', one: 'Samenwerker toevoegen', other: 'Samenwerkers toevoegen')}"; + + static String m2(count) => + "${Intl.plural(count, one: 'Bestand toevoegen', other: 'Bestanden toevoegen')}"; + static String m3(storageAmount, endDate) => "Jouw ${storageAmount} add-on is geldig tot ${endDate}"; + static String m4(count) => + "${Intl.plural(count, zero: 'Kijker toevoegen', one: 'Kijker toevoegen', other: 'Kijkers toevoegen')}"; + static String m5(emailOrName) => "Toegevoegd door ${emailOrName}"; static String m6(albumName) => "Succesvol toegevoegd aan ${albumName}"; @@ -90,208 +99,230 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} bestanden, elk ${formattedSize}"; - static String m27(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; + static String m27(name) => "Dit e-mailadres is al aan ${name} gekoppeld."; - static String m28(email) => "${email} heeft geen Ente account."; + static String m28(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} heeft geen Ente account."; + + static String m30(email) => "${email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto\'s te delen."; - static String m30(name) => "${name} omarmen"; + static String m31(name) => "${name} omarmen"; - static String m31(text) => "Extra foto\'s gevonden voor ${text}"; + static String m32(text) => "Extra foto\'s gevonden voor ${text}"; - static String m32(name) => "Feestmaal met ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; + static String m33(name) => "Feestmaal met ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album is veilig geback-upt"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast"; - static String m36(endDate) => "Gratis proefversie geldig tot ${endDate}"; + static String m37(endDate) => "Gratis proefversie geldig tot ${endDate}"; - static String m38(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + static String m38(count) => + "Je hebt nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op Ente zolang je een actief abonnement hebt"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Het kan verwijderd worden van het apparaat om ${formattedSize} vrij te maken', other: 'Ze kunnen verwijderd worden van het apparaat om ${formattedSize} vrij te maken')}"; + + static String m41(currentlyProcessing, totalCount) => "Verwerken van ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Wandelen met ${name}"; + static String m42(name) => "Wandelen met ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(name) => "Laatste keer met ${name}"; + static String m44(name) => "Laatste keer met ${name}"; - static String m44(email) => + static String m45(email) => "${email} heeft je uitgenodigd om een vertrouwd contact te zijn"; - static String m45(expiryTime) => "Link vervalt op ${expiryTime}"; + static String m46(expiryTime) => "Link vervalt op ${expiryTime}"; - static String m46(email) => "Link persoon aan ${email}"; + static String m47(email) => "Link persoon aan ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Dit linkt ${personName} aan ${email}"; - static String m50(albumName) => "Succesvol verplaatst naar ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'geen herinneringen', one: '${formattedCount} herinnering', other: '${formattedCount} herinneringen')}"; - static String m51(personName) => "Geen suggesties voor ${personName}"; + static String m50(count) => + "${Intl.plural(count, one: 'Bestand verplaatsen', other: 'Bestanden verplaatsen')}"; - static String m52(name) => "Niet ${name}?"; + static String m51(albumName) => "Succesvol verplaatst naar ${albumName}"; - static String m53(familyAdminEmail) => + static String m52(personName) => "Geen suggesties voor ${personName}"; + + static String m53(name) => "Niet ${name}?"; + + static String m54(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw code te wijzigen."; - static String m54(name) => "Feest met ${name}"; + static String m55(name) => "Feest met ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Wachtwoord sterkte: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Praat met ${providerName} klantenservice als u in rekening bent gebracht"; - static String m57(name, age) => "${name} is ${age}!"; + static String m58(name, age) => "${name} is ${age}!"; - static String m58(name, age) => "${name} wordt binnenkort ${age}"; + static String m59(name, age) => "${name} wordt binnenkort ${age}"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'Geen foto\'s', one: '1 foto', other: '${count} foto\'s')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 foto\'s', one: '1 foto', other: '${count} foto\'s')}"; + + static String m62(endDate) => "Gratis proefperiode geldig tot ${endDate}.\nU kunt naderhand een betaald abonnement kiezen."; - static String m62(toEmail) => "Stuur ons een e-mail op ${toEmail}"; + static String m63(toEmail) => "Stuur ons een e-mail op ${toEmail}"; - static String m63(toEmail) => + static String m64(toEmail) => "Verstuur de logboeken alstublieft naar ${toEmail}"; - static String m64(name) => "Poseren met ${name}"; + static String m65(name) => "Poseren met ${name}"; - static String m65(folderName) => "Verwerken van ${folderName}..."; + static String m66(folderName) => "Verwerken van ${folderName}..."; - static String m66(storeName) => "Beoordeel ons op ${storeName}"; + static String m67(storeName) => "Beoordeel ons op ${storeName}"; - static String m67(name) => "Toegewezen aan ${name}"; + static String m68(name) => "Toegewezen aan ${name}"; - static String m68(days, email) => + static String m69(days, email) => "U krijgt toegang tot het account na ${days} dagen. Een melding zal worden verzonden naar ${email}."; - static String m69(email) => + static String m70(email) => "U kunt nu het account van ${email} herstellen door een nieuw wachtwoord in te stellen."; - static String m70(email) => "${email} probeert je account te herstellen."; + static String m71(email) => "${email} probeert je account te herstellen."; - static String m71(storageInGB) => + static String m72(storageInGB) => "Jullie krijgen allebei ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto\'s worden ook uit het album verwijderd"; - static String m73(endDate) => "Wordt verlengd op ${endDate}"; + static String m74(endDate) => "Wordt verlengd op ${endDate}"; - static String m74(name) => "Roadtrip met ${name}"; + static String m75(name) => "Roadtrip met ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultaat gevonden', other: '${count} resultaten gevonden')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Lengte van secties komt niet overeen: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} geselecteerd"; + static String m78(count) => "${count} geselecteerd"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} geselecteerd (${yourCount} van jou)"; - static String m78(name) => "Selfies met ${name}"; + static String m80(name) => "Selfies met ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente verwijzingscode: ${referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om ${referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}"; - static String m83(emailIDs) => "Gedeeld met ${emailIDs}"; - - static String m84(fileType) => - "Deze ${fileType} zal worden verwijderd van jouw apparaat."; - - static String m85(fileType) => - "Deze ${fileType} staat zowel in Ente als op jouw apparaat."; + static String m85(emailIDs) => "Gedeeld met ${emailIDs}"; static String m86(fileType) => + "Deze ${fileType} zal worden verwijderd van jouw apparaat."; + + static String m87(fileType) => + "Deze ${fileType} staat zowel in Ente als op jouw apparaat."; + + static String m88(fileType) => "Deze ${fileType} zal worden verwijderd uit Ente."; - static String m87(name) => "Sporten met ${name}"; + static String m89(name) => "Sporten met ${name}"; - static String m88(name) => "Spotlicht op ${name}"; + static String m90(name) => "Spotlicht op ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt"; - static String m91(id) => + static String m93(id) => "Jouw ${id} is al aan een ander Ente account gekoppeld.\nAls je jouw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice"; - static String m92(endDate) => "Uw abonnement loopt af op ${endDate}"; + static String m94(endDate) => "Uw abonnement loopt af op ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} herinneringen bewaard"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Tik om te uploaden, upload wordt momenteel genegeerd vanwege ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Zij krijgen ook ${storageAmountInGB} GB"; - static String m96(email) => "Dit is de verificatie-ID van ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'Deze week, ${count} jaar geleden', other: 'Deze week, ${count} jaren geleden')}"; - - static String m98(dateFormat) => "${dateFormat} door de jaren"; + static String m98(email) => "Dit is de verificatie-ID van ${email}"; static String m99(count) => + "${Intl.plural(count, one: 'Deze week, ${count} jaar geleden', other: 'Deze week, ${count} jaren geleden')}"; + + static String m100(dateFormat) => "${dateFormat} door de jaren"; + + static String m101(count) => "${Intl.plural(count, zero: 'Binnenkort', one: '1 dag', other: '${count} dagen')}"; - static String m100(year) => "Reis in ${year}"; + static String m102(year) => "Reis in ${year}"; - static String m101(location) => "Reis naar ${location}"; + static String m103(location) => "Reis naar ${location}"; - static String m102(email) => + static String m104(email) => "Je bent uitgenodigd om een legacy contact van ${email} te zijn."; - static String m103(galleryType) => + static String m105(galleryType) => "Galerijtype ${galleryType} wordt niet ondersteund voor hernoemen"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Upload wordt genegeerd omdat ${ignoreReason}"; - static String m105(count) => "${count} herinneringen veiligstellen..."; + static String m107(count) => "${count} herinneringen veiligstellen..."; - static String m106(endDate) => "Geldig tot ${endDate}"; + static String m108(endDate) => "Geldig tot ${endDate}"; - static String m107(email) => "Verifieer ${email}"; + static String m109(email) => "Verifieer ${email}"; - static String m109(email) => + static String m110(name) => "Bekijk ${name} om te ontkoppelen"; + + static String m111(count) => + "${Intl.plural(count, zero: '0 kijkers toegevoegd', one: '1 kijker toegevoegd', other: '${count} kijkers toegevoegd')}"; + + static String m112(email) => "We hebben een e-mail gestuurd naar ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} jaar geleden', other: '${count} jaar geleden')}"; - static String m111(name) => "Jij en ${name}"; + static String m114(name) => "Jij en ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Je hebt ${storageSaved} succesvol vrijgemaakt!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -317,9 +348,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuw e-mailadres toevoegen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Samenwerker toevoegen"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Bestanden toevoegen"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Toevoegen vanaf apparaat"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Locatie toevoegen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Toevoegen"), @@ -345,6 +378,7 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Vertrouwd contact toevoegen"), "addViewer": MessageLookupByLibrary.simpleMessage("Voeg kijker toe"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Voeg nu je foto\'s toe"), "addedAs": MessageLookupByLibrary.simpleMessage("Toegevoegd als"), @@ -515,26 +549,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Back-up video\'s"), "beach": MessageLookupByLibrary.simpleMessage("Zand en zee"), "birthday": MessageLookupByLibrary.simpleMessage("Verjaardag"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday-aanbieding"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Bulk datums wijzigen"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Je kunt nu meerdere foto\'s selecteren en de datum/tijd van ze allemaal bewerken met één snelle actie. Verschuiven van datums wordt ook ondersteund."), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Familieplan limieten"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Je kunt nu limieten instellen voor hoeveel opslag je familieleden kunnen gebruiken."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nieuw icoon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Tot slot, een nieuwe app icoon waarvan we denken dat het ons werk het beste weergeeft. We hebben ook een icon-switcher toegevoegd zodat je het oude icoon kunt blijven gebruiken."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Herinneringen"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Ontdek nog eens je speciale momenten - spotlicht op je favoriete mensen, op je reizen en vakanties, op de beste plaatjes, en nog veel meer. Zet Machine Learning aan, tag jezelf en benoem je vrienden voor de beste ervaring."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets die zijn geïntegreerd met herinneringen zijn nu beschikbaar. Ze tonen je speciale momenten zonder de app te openen."), "cachedData": MessageLookupByLibrary.simpleMessage("Cachegegevens"), "calculating": MessageLookupByLibrary.simpleMessage("Berekenen..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -604,6 +622,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Click"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Klik op het menu"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Sluiten"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Samenvoegen op tijd"), @@ -840,6 +860,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), "editLocationTagTitle": @@ -855,16 +876,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail is al geregistreerd."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail niet geregistreerd."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-mailverificatie"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("E-mail uw logboeken"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Noodcontacten"), "empty": MessageLookupByLibrary.simpleMessage("Leeg"), @@ -928,6 +949,8 @@ class MessageLookup extends MessageLookupByLibrary { "Voer een geldig e-mailadres in."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("Voer je e-mailadres in"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Voer uw nieuwe e-mailadres in"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Voer je wachtwoord in"), "enterYourRecoveryKey": @@ -945,7 +968,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra foto\'s gevonden"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gezicht nog niet geclusterd, kom later terug"), "faceRecognition": @@ -985,7 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary { "faqs": MessageLookupByLibrary.simpleMessage("Veelgestelde vragen"), "favorite": MessageLookupByLibrary.simpleMessage("Toevoegen aan favorieten"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("Bestand"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -999,8 +1022,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Bestandstype"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Bestandstypen en namen"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -1018,24 +1041,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gezichten gevonden"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis opslag geclaimd"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Gratis opslag bruikbaar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis proefversie"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Apparaatruimte vrijmaken"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Bespaar ruimte op je apparaat door bestanden die al geback-upt zijn te wissen."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Ruimte vrijmaken"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galerij"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Tot 1000 herinneringen getoond in de galerij"), "general": MessageLookupByLibrary.simpleMessage("Algemeen"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Encryptiesleutels genereren..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ga naar instellingen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1049,6 +1074,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Gasten weergave"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Om gasten weergave in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1064,7 +1091,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Verberg gedeelde bestanden uit de galerij"), "hiding": MessageLookupByLibrary.simpleMessage("Verbergen..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Gehost bij OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Hoe het werkt"), @@ -1121,7 +1148,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"), @@ -1142,7 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Help ons alsjeblieft met deze informatie"), "language": MessageLookupByLibrary.simpleMessage("Taal"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Laatst gewijzigd"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Reis van vorig jaar"), @@ -1156,7 +1183,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy geeft vertrouwde contacten toegang tot je account bij afwezigheid."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1173,7 +1200,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("voor sneller delen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ingeschakeld"), "linkExpired": MessageLookupByLibrary.simpleMessage("Verlopen"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Vervaldatum"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link is vervallen"), @@ -1181,13 +1208,13 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Link persoon"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "voor een betere ervaring met delen"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live foto"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "U kunt uw abonnement met uw familie delen"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "We hebben tot nu toe meer dan tien miljoen herinneringen bewaard"), + "We hebben tot nu toe meer dan 200 miljoen herinneringen bewaard"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "We bewaren 3 kopieën van uw bestanden, één in een ondergrondse kernbunker"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1242,6 +1269,8 @@ class MessageLookup extends MessageLookupByLibrary { "Druk lang op een e-mail om de versleuteling te verifiëren."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Houd een bestand lang ingedrukt om te bekijken op volledig scherm"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Video in lus afspelen uit"), "loopVideoOn": @@ -1272,6 +1301,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ik"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Samenvoegen met bestaand"), @@ -1303,13 +1333,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Meest recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Meest relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over de heuvels"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Verplaats de geselecteerde foto\'s naar één datum"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Verplaats naar album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Verplaatsen naar verborgen album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Naar prullenbak verplaatst"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1324,6 +1355,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Nieuw album"), "newLocation": MessageLookupByLibrary.simpleMessage("Nieuwe locatie"), "newPerson": MessageLookupByLibrary.simpleMessage("Nieuw persoon"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Nieuwe reeks"), "newToEnte": MessageLookupByLibrary.simpleMessage("Nieuw bij Ente"), "newest": MessageLookupByLibrary.simpleMessage("Nieuwste"), @@ -1364,10 +1396,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Geen resultaten"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Geen resultaten gevonden"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Geen systeemvergrendeling gevonden"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Niet dezelfde persoon?"), "nothingSharedWithYouYet": @@ -1380,7 +1412,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "Op ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Onderweg"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Alleen hen"), "oops": MessageLookupByLibrary.simpleMessage("Oeps"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1410,7 +1445,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Koppeling voltooid"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "Verificatie is nog in behandeling"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), @@ -1420,7 +1455,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Wachtwoord succesvol aangepast"), "passwordLock": MessageLookupByLibrary.simpleMessage("Wachtwoord slot"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "De wachtwoordsterkte wordt berekend aan de hand van de lengte van het wachtwoord, de gebruikte tekens en of het wachtwoord al dan niet in de top 10.000 van meest gebruikte wachtwoorden staat"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1431,7 +1466,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Betaling mislukt"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Helaas is je betaling mislukt. Neem contact op met support zodat we je kunnen helpen!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"), "pendingSync": MessageLookupByLibrary.simpleMessage( @@ -1445,20 +1480,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Permanent verwijderen"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Permanent verwijderen van apparaat?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Naam van persoon"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Harige kameraden"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Foto beschrijvingen"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Foto raster grootte"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Foto\'s"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Foto\'s toegevoegd door u zullen worden verwijderd uit het album"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Foto\'s behouden relatief tijdsverschil"), @@ -1471,7 +1507,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Origineel afspelen"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Stream afspelen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), @@ -1484,14 +1520,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Neem contact op met klantenservice als het probleem aanhoudt"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Geef alstublieft toestemming"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Log opnieuw in"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecteer snelle links om te verwijderen"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Probeer het nog eens"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1506,7 +1542,7 @@ class MessageLookup extends MessageLookupByLibrary { "Gelieve even te wachten voordat u opnieuw probeert"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Een ogenblik geduld, dit zal even duren."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Logboeken voorbereiden..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Meer bewaren"), @@ -1524,7 +1560,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Verder"), "processed": MessageLookupByLibrary.simpleMessage("Verwerkt"), "processing": MessageLookupByLibrary.simpleMessage("Verwerken"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Video\'s verwerken"), "publicLinkCreated": @@ -1537,12 +1573,14 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Meld probleem"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Beoordeel de app"), "rateUs": MessageLookupByLibrary.simpleMessage("Beoordeel ons"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("\"Ik\" opnieuw toewijzen"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Opnieuw toewijzen..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Herstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Account herstellen"), @@ -1551,7 +1589,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Account herstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Herstel gestart"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Herstelsleutel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Herstelsleutel gekopieerd naar klembord"), @@ -1565,12 +1603,12 @@ class MessageLookup extends MessageLookupByLibrary { "Herstel sleutel geverifieerd"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Je herstelsleutel is de enige manier om je foto\'s te herstellen als je je wachtwoord bent vergeten. Je vindt je herstelsleutel in Instellingen > Account.\n\nVoer hier je herstelsleutel in om te controleren of je hem correct hebt opgeslagen."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Herstel succesvol!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Een vertrouwd contact probeert toegang te krijgen tot je account"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Het huidige apparaat is niet krachtig genoeg om je wachtwoord te verifiëren, dus moeten we de code een keer opnieuw genereren op een manier die met alle apparaten werkt.\n\nLog in met behulp van uw herstelcode en genereer opnieuw uw wachtwoord (je kunt dezelfde indien gewenst opnieuw gebruiken)."), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage( @@ -1586,7 +1624,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Geef deze code aan je vrienden"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ze registreren voor een betaald plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referenties"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Verwijzingen zijn momenteel gepauzeerd"), @@ -1618,7 +1656,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Verwijder link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Verwijder persoonslabel"), "removePublicLink": @@ -1640,7 +1678,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bestandsnaam wijzigen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement verlengen"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Een fout melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fout melden"), "resendEmail": @@ -1666,7 +1704,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Suggesties beoordelen"), "right": MessageLookupByLibrary.simpleMessage("Rechts"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Roteren"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Roteer links"), "rotateRight": MessageLookupByLibrary.simpleMessage("Rechtsom draaien"), @@ -1722,8 +1760,8 @@ class MessageLookup extends MessageLookupByLibrary { "Nodig mensen uit, en je ziet alle foto\'s die door hen worden gedeeld hier"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Personen worden hier getoond zodra verwerking en synchroniseren voltooid is"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Bekijk publieke album links in de app"), @@ -1772,9 +1810,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden worden van deze persoon verwijderd, maar niet uit uw bibliotheek verwijderd."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Verzenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-mail versturen"), "sendInvite": @@ -1806,16 +1844,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Deel nu een album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link delen"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Deel alleen met de mensen die u wilt"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente zodat we gemakkelijk foto\'s en video\'s in originele kwaliteit kunnen delen\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Delen met niet-Ente gebruikers"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1826,7 +1864,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuwe gedeelde foto\'s"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ontvang meldingen wanneer iemand een foto toevoegt aan een gedeeld album waar je deel van uitmaakt"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Gedeeld met mij"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Gedeeld met jou"), @@ -1844,11 +1882,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Log uit op andere apparaten"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ik ga akkoord met de gebruiksvoorwaarden en privacybeleid"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Het wordt uit alle albums verwijderd."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Overslaan"), "social": MessageLookupByLibrary.simpleMessage("Sociale media"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1865,6 +1903,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Er is iets fout gegaan, probeer het opnieuw"), "sorry": MessageLookupByLibrary.simpleMessage("Sorry"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Sorry, we kunnen dit bestand nu niet back-uppen, we zullen het later opnieuw proberen."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Sorry, kon niet aan favorieten worden toegevoegd!"), "sorryCouldNotRemoveFromFavorites": @@ -1876,14 +1916,16 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Sorry, we konden geen beveiligde sleutels genereren op dit apparaat.\n\nGelieve je aan te melden vanaf een ander apparaat."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sorteren"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sorteren op"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Nieuwste eerst"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Oudste eerst"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Succes"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Spotlicht op jezelf"), "startAccountRecoveryTitle": @@ -1897,14 +1939,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Opslagruimte"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jij"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Opslaglimiet overschreden"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Sterk"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonneer"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Je hebt een actief betaald abonnement nodig om delen mogelijk te maken."), @@ -1922,7 +1964,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Features voorstellen"), "sunrise": MessageLookupByLibrary.simpleMessage("Aan de horizon"), "support": MessageLookupByLibrary.simpleMessage("Ondersteuning"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisatie gestopt"), "syncing": MessageLookupByLibrary.simpleMessage("Synchroniseren..."), @@ -1934,7 +1976,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tik om te ontgrendelen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tik om te uploaden"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam."), "terminate": MessageLookupByLibrary.simpleMessage("Beëindigen"), @@ -1958,7 +2000,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Deze bestanden zullen worden verwijderd van uw apparaat."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ze zullen uit alle albums worden verwijderd."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1976,12 +2018,12 @@ class MessageLookup extends MessageLookupByLibrary { "Deze foto heeft geen exif gegevens"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Dit ben ik!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Dit is uw verificatie-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Deze week door de jaren"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dit zal je uitloggen van het volgende apparaat:"), @@ -1993,7 +2035,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Hiermee worden openbare links van alle geselecteerde snelle links verwijderd."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Om appvergrendeling in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."), @@ -2008,13 +2050,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totaal"), "totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"), "trash": MessageLookupByLibrary.simpleMessage("Prullenbak"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Knippen"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrouwde contacten"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Probeer opnieuw"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Schakel back-up in om bestanden die toegevoegd zijn aan deze map op dit apparaat automatisch te uploaden."), @@ -2033,7 +2075,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tweestapsverificatie succesvol gereset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Tweestapsverificatie"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Uit archief halen"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album uit archief halen"), @@ -2059,10 +2101,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Map selectie bijwerken..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgraden"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Bestanden worden geüpload naar album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "1 herinnering veiligstellen..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2080,7 +2122,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gebruik geselecteerde foto"), "usedSpace": MessageLookupByLibrary.simpleMessage("Gebruikte ruimte"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificatie mislukt, probeer het opnieuw"), @@ -2088,7 +2130,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verificatie ID"), "verify": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Bevestig passkey"), @@ -2100,7 +2142,7 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Video-info"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Video streamen"), + MessageLookupByLibrary.simpleMessage("Video\'s met stream"), "videos": MessageLookupByLibrary.simpleMessage("Video\'s"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Actieve sessies bekijken"), @@ -2114,9 +2156,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Bekijk bestanden die de meeste opslagruimte verbruiken."), "viewLogs": MessageLookupByLibrary.simpleMessage("Logboeken bekijken"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Toon herstelsleutel"), "viewer": MessageLookupByLibrary.simpleMessage("Kijker"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bezoek alstublieft web.ente.io om uw abonnement te beheren"), "waitingForVerification": @@ -2129,7 +2173,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We ondersteunen het bewerken van foto\'s en albums waar je niet de eigenaar van bent nog niet"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Zwak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welkom terug!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nieuw"), @@ -2137,7 +2181,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vertrouwde contacten kunnen helpen bij het herstellen van je data."), "yearShort": MessageLookupByLibrary.simpleMessage("jr"), "yearly": MessageLookupByLibrary.simpleMessage("Jaarlijks"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, opzeggen"), "yesConvertToViewer": @@ -2151,7 +2195,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, reset persoon"), "you": MessageLookupByLibrary.simpleMessage("Jij"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "U bent onderdeel van een familie abonnement!"), "youAreOnTheLatestVersion": @@ -2170,7 +2214,7 @@ class MessageLookup extends MessageLookupByLibrary { "Je kunt niet met jezelf delen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "U heeft geen gearchiveerde bestanden."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Je account is verwijderd"), "yourMap": MessageLookupByLibrary.simpleMessage("Jouw kaart"), diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index e4905563a0..4f57a9d1a5 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -22,6 +22,9 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Me)"; + static String m2(count) => + "${Intl.plural(count, one: 'Legg til element', other: 'Legg til elementene')}"; + static String m3(storageAmount, endDate) => "Tillegget på ${storageAmount} er gyldig til ${endDate}"; @@ -90,205 +93,211 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} filer, ${formattedSize} hver"; - static String m27(newEmail) => "E-postadressen er endret til ${newEmail}"; + static String m28(newEmail) => "E-postadressen er endret til ${newEmail}"; - static String m28(email) => "${email} har ikke en Ente-konto."; + static String m29(email) => "${email} har ikke en Ente-konto."; - static String m29(email) => + static String m30(email) => "${email} har ikke en Ente-konto.\n\nsender dem en invitasjon til å dele bilder."; - static String m30(name) => "Omfavner ${name}"; + static String m31(name) => "Omfavner ${name}"; - static String m31(text) => "Ekstra bilder funnet for ${text}"; + static String m32(text) => "Ekstra bilder funnet for ${text}"; - static String m32(name) => "Festing med ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} på denne enheten har blitt sikkerhetskopiert"; + static String m33(name) => "Festing med ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} på denne enheten har blitt sikkerhetskopiert"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 fil', other: '${formattedNumber} filer')} I dette albumet har blitt sikkerhetskopiert"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB hver gang noen melder seg på en betalt plan og bruker koden din"; - static String m36(endDate) => "Prøveperioden varer til ${endDate}"; + static String m37(endDate) => "Prøveperioden varer til ${endDate}"; - static String m38(sizeInMBorGB) => "Frigjør ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Frigjør ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Behandler ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Tur med ${name}"; + static String m42(name) => "Tur med ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} element', other: '${count} elementer')}"; - static String m43(name) => "Siste gang med ${name}"; + static String m44(name) => "Siste gang med ${name}"; - static String m44(email) => + static String m45(email) => "${email} har invitert deg til å være en betrodd kontakt"; - static String m45(expiryTime) => "Lenken utløper på ${expiryTime}"; + static String m46(expiryTime) => "Lenken utløper på ${expiryTime}"; - static String m46(email) => "Knytt personen til ${email}"; + static String m47(email) => "Knytt personen til ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Dette knytter ${personName} til ${email}"; - static String m50(albumName) => "Flyttet til ${albumName}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'ingen minner', one: '${formattedCount} minne', other: '${formattedCount} minner')}"; - static String m51(personName) => "Ingen forslag for ${personName}"; + static String m50(count) => + "${Intl.plural(count, one: 'Flytt elementet', other: 'Flytt elementene')}"; - static String m52(name) => "Ikke ${name}?"; + static String m51(albumName) => "Flyttet til ${albumName}"; - static String m53(familyAdminEmail) => + static String m52(personName) => "Ingen forslag for ${personName}"; + + static String m53(name) => "Ikke ${name}?"; + + static String m54(familyAdminEmail) => "Vennligst kontakt ${familyAdminEmail} for å endre koden din."; - static String m54(name) => "Fest med ${name}"; + static String m55(name) => "Fest med ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Passordstyrke: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Snakk med ${providerName} kundestøtte hvis du ble belastet"; - static String m57(name, age) => "${name} er ${age}!"; + static String m58(name, age) => "${name} er ${age}!"; - static String m58(name, age) => "${name} fyller ${age} snart"; + static String m59(name, age) => "${name} fyller ${age} snart"; - static String m59(count) => + static String m60(count) => "${Intl.plural(count, zero: 'Ingen bilder', one: '1 bilde', other: '${count} bilder')}"; - static String m61(endDate) => + static String m62(endDate) => "Prøveperioden varer til ${endDate}.\nDu kan velge en betalt plan etterpå."; - static String m62(toEmail) => "Vennligst send oss en e-post på ${toEmail}"; + static String m63(toEmail) => "Vennligst send oss en e-post på ${toEmail}"; - static String m63(toEmail) => "Vennligst send loggene til \n${toEmail}"; + static String m64(toEmail) => "Vennligst send loggene til \n${toEmail}"; - static String m64(name) => "Poseringer med ${name}"; + static String m65(name) => "Poseringer med ${name}"; - static String m65(folderName) => "Behandler ${folderName}..."; + static String m66(folderName) => "Behandler ${folderName}..."; - static String m66(storeName) => "Vurder oss på ${storeName}"; + static String m67(storeName) => "Vurder oss på ${storeName}"; - static String m67(name) => + static String m68(name) => "Tildeler deg til ${name}${name}${name}${name}${name}"; - static String m68(days, email) => + static String m69(days, email) => "Du kan få tilgang til kontoen etter ${days} dager. En varsling vil bli sendt til ${email}."; - static String m69(email) => + static String m70(email) => "Du kan nå gjenopprette ${email} sin konto ved å sette et nytt passord."; - static String m70(email) => "${email} prøver å gjenopprette kontoen din."; + static String m71(email) => "${email} prøver å gjenopprette kontoen din."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Begge dere får ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} vil bli fjernet fra dette delte albumet\n\nAlle bilder lagt til av dem vil også bli fjernet fra albumet"; - static String m73(endDate) => "Abonnement fornyes på ${endDate}"; + static String m74(endDate) => "Abonnement fornyes på ${endDate}"; - static String m74(name) => "Biltur med ${name}"; + static String m75(name) => "Biltur med ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultat funnet', other: '${count} resultater funnet')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Uoverensstemmelse i seksjonslengde: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} valgt"; + static String m78(count) => "${count} valgt"; - static String m77(count, yourCount) => "${count} valgt (${yourCount} dine)"; + static String m79(count, yourCount) => "${count} valgt (${yourCount} dine)"; - static String m78(name) => "Selfier med ${name}"; + static String m80(name) => "Selfier med ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Her er min verifiserings-ID: ${verificationID} for ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hei, kan du bekrefte at dette er din ente.io verifiserings-ID: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Gi vervekode: ${referralCode} \n\nBruk den i Innstillinger → General → Verving for å få ${referralStorageInGB} GB gratis etter at du har registrert deg for en betalt plan\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Del med bestemte personer', one: 'Delt med 1 person', other: 'Delt med ${numberOfPeople} personer')}"; - static String m83(emailIDs) => "Delt med ${emailIDs}"; + static String m85(emailIDs) => "Delt med ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Denne ${fileType} vil bli slettet fra enheten din."; - static String m85(fileType) => + static String m87(fileType) => "Denne ${fileType} er både i Ente og på enheten din."; - static String m86(fileType) => "Denne ${fileType} vil bli slettet fra Ente."; + static String m88(fileType) => "Denne ${fileType} vil bli slettet fra Ente."; - static String m87(name) => "Sport med ${name}"; + static String m89(name) => "Sport med ${name}"; - static String m88(name) => "Fremhev ${name}"; + static String m90(name) => "Fremhev ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} av ${totalAmount} ${totalStorageUnit} brukt"; - static String m91(id) => + static String m93(id) => "Din ${id} er allerede koblet til en annen Ente-konto.\nHvis du ønsker å bruke din ${id} med denne kontoen, vennligst kontakt vår brukerstøtte\'\'"; - static String m92(endDate) => + static String m94(endDate) => "Abonnementet ditt blir avsluttet den ${endDate}"; - static String m93(completed, total) => "${completed}/${total} minner bevart"; + static String m95(completed, total) => "${completed}/${total} minner bevart"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Trykk for å laste opp, opplasting er ignorert nå på grunn av ${ignoreReason}"; - static String m95(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "De får også ${storageAmountInGB} GB"; - static String m96(email) => "Dette er ${email} sin verifiserings-ID"; - - static String m97(count) => - "${Intl.plural(count, one: 'Denne uka, ${count} år siden', other: 'Denne uka, ${count} år siden')}"; - - static String m98(dateFormat) => "${dateFormat} gjennom årene"; + static String m98(email) => "Dette er ${email} sin verifiserings-ID"; static String m99(count) => + "${Intl.plural(count, one: 'Denne uka, ${count} år siden', other: 'Denne uka, ${count} år siden')}"; + + static String m100(dateFormat) => "${dateFormat} gjennom årene"; + + static String m101(count) => "${Intl.plural(count, zero: 'Snart', one: '1 dag', other: '${count} dager')}"; - static String m100(year) => "Reise i ${year}"; + static String m102(year) => "Reise i ${year}"; - static String m101(location) => "Reise til ${location}"; + static String m103(location) => "Reise til ${location}"; - static String m102(email) => + static String m104(email) => "Du er invitert til å være en betrodd kontakt av ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Galleritype ${galleryType} støttes ikke for nytt navn"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Opplastingen ble ignorert på grunn av ${ignoreReason}"; - static String m105(count) => "Bevarer ${count} minner..."; + static String m107(count) => "Bevarer ${count} minner..."; - static String m106(endDate) => "Gyldig til ${endDate}"; + static String m108(endDate) => "Gyldig til ${endDate}"; - static String m107(email) => "Verifiser ${email}"; + static String m109(email) => "Verifiser ${email}"; - static String m109(email) => + static String m112(email) => "Vi har sendt en e-post til ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} år siden', other: '${count} år siden')}"; - static String m111(name) => "Du og ${name}"; + static String m114(name) => "Du og ${name}"; - static String m112(storageSaved) => "Du har frigjort ${storageSaved}!"; + static String m115(storageSaved) => "Du har frigjort ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -315,6 +324,7 @@ class MessageLookup extends MessageLookupByLibrary { "addFiles": MessageLookupByLibrary.simpleMessage("Legg til filer"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Legg til fra enhet"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Legg til sted"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Legg til"), "addMore": MessageLookupByLibrary.simpleMessage("Legg til flere"), @@ -508,26 +518,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sikkerhetskopier videoer"), "beach": MessageLookupByLibrary.simpleMessage("Sand og sjø"), "birthday": MessageLookupByLibrary.simpleMessage("Bursdag"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Black Friday salg"), "blog": MessageLookupByLibrary.simpleMessage("Blogg"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Masseendring av datoer"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Du kan nå velge flere bilder, og redigere dato/klokkeslett for alle med en rask handling. Forskyving av datoer støttes også."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Begrensninger for familieabonnement"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Du kan nå sette grenser for hvor mye lagringsplass familiemedlemmer kan bruke."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Nytt ikon"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Endelig er et nytt appikon, som vi tror best representerer arbeidet vårt. Vi har også lagt til en icon-switcher slik at du kan fortsette å bruke det gamle ikonet."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Minner"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Gjenoppdag dine spesielle øyeblikk - fremhev dine favorittpersoner, dine turer og ferier, de beste bildene dine, og mye mer. Skru på maskinlæring, merk deg selv og navngi vennene dine for best mulig opplevelse."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgeter"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Hjemmeskjermwidgeter som er integrert med minner er nå tilgjengelige. De vil vise dine spesielle øyeblikk uten å åpne appen."), "cachedData": MessageLookupByLibrary.simpleMessage("Bufrede data"), "calculating": MessageLookupByLibrary.simpleMessage("Beregner..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( @@ -597,6 +591,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Klikk"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Klikk på menyen med tre prikker"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Lukk"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Grupper etter tidspunkt for opptak"), @@ -843,16 +839,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-post"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen er allerede registrert."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen er ikke registrert."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-postbekreftelse"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Send loggene dine på e-post"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Nødkontakter"), "empty": MessageLookupByLibrary.simpleMessage("Tom"), @@ -928,7 +924,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksporter dine data"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Ekstra bilder funnet"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Ansikt ikke gruppert ennå, vennligst kom tilbake senere"), "faceRecognition": @@ -967,7 +963,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Ofte stilte spørsmål"), "faqs": MessageLookupByLibrary.simpleMessage("Ofte stilte spørsmål"), "favorite": MessageLookupByLibrary.simpleMessage("Favoritt"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "file": MessageLookupByLibrary.simpleMessage("Fil"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -981,8 +977,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Filtyper"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Filtyper og navn"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Filene er slettet"), "filesSavedToGallery": @@ -999,13 +995,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Fant ansikter"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Gratis lagringplass aktivert"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Gratis lagringsplass som kan brukes"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis prøveversjon"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Frigjør plass på enheten"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -1018,7 +1014,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Generelt"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Genererer krypteringsnøkler..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Gå til innstillinger"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1032,6 +1028,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Gjestevisning"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "For å aktivere gjestevisning, vennligst konfigurer enhetens passord eller skjermlås i systeminnstillingene."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Vi sporer ikke app-installasjoner. Det hadde vært til hjelp om du fortalte oss hvor du fant oss!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1047,7 +1045,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Skjul delte elementer fra hjemgalleriet"), "hiding": MessageLookupByLibrary.simpleMessage("Skjuler..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hostet på OSM France"), "howItWorks": @@ -1104,7 +1102,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Det ser ut til at noe gikk galt. Prøv på nytt etter en stund. Hvis feilen vedvarer, kan du kontakte kundestøtte."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementer viser gjenværende dager før de slettes for godt"), @@ -1125,7 +1123,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), "language": MessageLookupByLibrary.simpleMessage("Språk"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Sist oppdatert"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("Fjorårets tur"), "leave": MessageLookupByLibrary.simpleMessage("Forlat"), @@ -1136,7 +1134,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("Venstre"), "legacy": MessageLookupByLibrary.simpleMessage("Arv"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Eldre kontoer"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Arv-funksjonen lar betrodde kontakter få tilgang til kontoen din i ditt fravær."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1152,7 +1150,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("for raskere deling"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktivert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Utløpt"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Lenkeutløp"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Lenken har utløpt"), @@ -1160,13 +1158,11 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Knytt til person"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("for bedre delingsopplevelse"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live-bilder"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kan dele abonnementet med familien din"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Vi har bevart over 30 millioner minner så langt"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Vi beholder 3 kopier av dine data, en i en underjordisk bunker"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1222,6 +1218,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Lang-trykk på en gjenstand for å vise i fullskjerm"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Gjenta video av"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Gjenta video på"), "lostDevice": MessageLookupByLibrary.simpleMessage("Mistet enhet?"), @@ -1248,6 +1246,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Meg"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Varer"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Slå sammen med eksisterende"), @@ -1279,12 +1278,13 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Nyeste"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mest relevant"), "mountains": MessageLookupByLibrary.simpleMessage("Over åsene"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Flytt valgte bilder til en dato"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Flytt til album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Flytt til skjult album"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Flyttet til papirkurven"), "movingFilesToAlbum": @@ -1299,6 +1299,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Nytt album"), "newLocation": MessageLookupByLibrary.simpleMessage("Ny plassering"), "newPerson": MessageLookupByLibrary.simpleMessage("Ny person"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Ny rekkevidde"), "newToEnte": MessageLookupByLibrary.simpleMessage("Ny til Ente"), "newest": MessageLookupByLibrary.simpleMessage("Nyeste"), @@ -1338,10 +1339,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Ingen resultater"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Ingen resultater funnet"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Ingen systemlås funnet"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Ikke denne personen?"), "nothingSharedWithYouYet": @@ -1354,7 +1355,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "På ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("På veien igjen"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Bare de"), "oops": MessageLookupByLibrary.simpleMessage("Oisann"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1385,7 +1389,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Sammenkobling fullført"), "panorama": MessageLookupByLibrary.simpleMessage("Panora"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Bekreftelse venter fortsatt"), "passkey": MessageLookupByLibrary.simpleMessage("Tilgangsnøkkel"), @@ -1395,7 +1399,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Passordet ble endret"), "passwordLock": MessageLookupByLibrary.simpleMessage("Passordlås"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Passordstyrken beregnes basert på passordets lengde, brukte tegn, og om passordet finnes blant de 10 000 mest brukte passordene"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1406,7 +1410,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Betaling feilet"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Betalingen din mislyktes. Kontakt kundestøtte og vi vil hjelpe deg!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Ventende elementer"), "pendingSync": @@ -1420,16 +1424,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Slette for godt"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("Slett permanent fra enhet?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Personnavn"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Pelsvenner"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Bildebeskrivelser"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Bilderutenettstørrelse"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("bilde"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Bilder"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( @@ -1445,7 +1449,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Spill av album på TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Spill av original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Spill av strøm"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), @@ -1458,14 +1462,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vennligst kontakt kundestøtte hvis problemet vedvarer"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Vennligst gi tillatelser"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Vennligst logg inn igjen"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("Velg hurtiglenker å fjerne"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Vennligst prøv igjen"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1479,7 +1483,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vennligst vent en stund før du prøver på nytt"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Vennligst vent, dette vil ta litt tid."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Forbereder logger..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Behold mer"), @@ -1497,7 +1501,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Fortsett"), "processed": MessageLookupByLibrary.simpleMessage("Behandlet"), "processing": MessageLookupByLibrary.simpleMessage("Behandler"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Behandler videoer"), "publicLinkCreated": @@ -1510,11 +1514,13 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Opprett sak"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Vurder appen"), "rateUs": MessageLookupByLibrary.simpleMessage("Vurder oss"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Tildel \"Meg\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Tildeler..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Gjenopprett"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Gjenopprett konto"), @@ -1523,7 +1529,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gjenopprett konto"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Gjenoppretting startet"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Gjenopprettingsnøkkel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1538,12 +1544,12 @@ class MessageLookup extends MessageLookupByLibrary { "Gjenopprettingsnøkkel bekreftet"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Gjenopprettingsnøkkelen er den eneste måten å gjenopprette bildene dine på hvis du glemmer passordet ditt. Du finner gjenopprettingsnøkkelen din i Innstillinger > Konto.\n\nVennligst skriv inn gjenopprettingsnøkkelen din her for å bekrefte at du har lagret den riktig."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage( "Gjenopprettingen var vellykket!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "En betrodd kontakt prøver å få tilgang til kontoen din"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Den gjeldende enheten er ikke kraftig nok til å verifisere passordet ditt, men vi kan regenerere på en måte som fungerer på alle enheter.\n\nVennligst logg inn med gjenopprettingsnøkkelen og regenerer passordet (du kan bruke den samme igjen om du vil)."), "recreatePasswordTitle": @@ -1559,7 +1565,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gi denne koden til vennene dine"), "referralStep2": MessageLookupByLibrary.simpleMessage( "De registrerer seg for en betalt plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Vervinger"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Vervinger er for øyeblikket satt på pause"), @@ -1590,7 +1596,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Fjern lenke"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Fjern deltaker"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Fjern etikett for person"), "removePublicLink": @@ -1611,7 +1617,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Gi nytt filnavn"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Forny abonnement"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Rapporter en feil"), "reportBug": MessageLookupByLibrary.simpleMessage("Rapporter feil"), "resendEmail": @@ -1637,7 +1643,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Gjennomgå forslag"), "right": MessageLookupByLibrary.simpleMessage("Høyre"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Roter"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Roter mot venstre"), "rotateRight": MessageLookupByLibrary.simpleMessage("Roter mot høyre"), @@ -1692,8 +1698,8 @@ class MessageLookup extends MessageLookupByLibrary { "Inviter folk, og du vil se alle bilder som deles av dem her"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Folk vil vises her når behandling og synkronisering er fullført"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Sikkerhet"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Se offentlige albumlenker i appen"), @@ -1741,9 +1747,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Valgte elementer fjernes fra denne personen, men blir ikke slettet fra biblioteket ditt."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send e-post"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invitasjon"), @@ -1773,16 +1779,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Del et album nå"), "shareLink": MessageLookupByLibrary.simpleMessage("Del link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("Del bare med de du vil"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Last ned Ente slik at vi lett kan dele bilder og videoer av original kvalitet\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Del med brukere som ikke har Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Del ditt første album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1793,7 +1799,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nye delte bilder"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Motta varsler når noen legger til et bilde i et delt album som du er en del av"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Delt med meg"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Delt med deg"), "sharing": MessageLookupByLibrary.simpleMessage("Deler..."), @@ -1809,11 +1815,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Logg ut andre enheter"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Jeg godtar bruksvilkårene og personvernreglene"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Den vil bli slettet fra alle album."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Hopp over"), "social": MessageLookupByLibrary.simpleMessage("Sosial"), "someItemsAreInBothEnteAndYourDevice": @@ -1842,13 +1848,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Beklager, vi kunne ikke generere sikre nøkler på denne enheten.\n\nvennligst registrer deg fra en annen enhet."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sorter"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sorter etter"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Nyeste først"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Eldste først"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Suksess"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Fremhev deg selv"), "startAccountRecoveryTitle": @@ -1863,15 +1871,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Lagring"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Deg"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Lagringsplassen er full"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Strømmedetaljer"), "strongStrength": MessageLookupByLibrary.simpleMessage("Sterkt"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Du trenger et aktivt betalt abonnement for å aktivere deling."), @@ -1889,7 +1897,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Foreslå funksjoner"), "sunrise": MessageLookupByLibrary.simpleMessage("På horisonten"), "support": MessageLookupByLibrary.simpleMessage("Brukerstøtte"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synkronisering stoppet"), "syncing": MessageLookupByLibrary.simpleMessage("Synkroniserer..."), @@ -1902,7 +1910,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Trykk for å låse opp"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Trykk for å laste opp"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Det ser ut som noe gikk galt. Prøv på nytt etter en stund. Hvis feilen vedvarer, kontakt kundestøtte."), "terminate": MessageLookupByLibrary.simpleMessage("Avslutte"), @@ -1925,7 +1933,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Disse elementene vil bli slettet fra enheten din."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "De vil bli slettet fra alle album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1943,12 +1951,12 @@ class MessageLookup extends MessageLookupByLibrary { "Dette bildet har ingen exif-data"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Dette er meg!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dette er din bekreftelses-ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Denne uka gjennom årene"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Dette vil logge deg ut av følgende enhet:"), @@ -1960,7 +1968,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Dette fjerner de offentlige lenkene av alle valgte hurtiglenker."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "For å aktivere applås, vennligst angi passord eller skjermlås i systeminnstillingene."), @@ -1974,13 +1982,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totalt"), "totalSize": MessageLookupByLibrary.simpleMessage("Total størrelse"), "trash": MessageLookupByLibrary.simpleMessage("Papirkurv"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Beskjær"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Betrodde kontakter"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Prøv igjen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Slå på sikkerhetskopi for å automatisk laste opp filer lagt til denne enhetsmappen i Ente."), @@ -1998,7 +2006,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tofaktorautentisering ble tilbakestilt"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Oppsett av to-faktor"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Opphev arkivering"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Gjenopprett album"), @@ -2022,10 +2030,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Oppdaterer mappevalg..."), "upgrade": MessageLookupByLibrary.simpleMessage("Oppgrader"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Laster opp filer til albumet..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Bevarer 1 minne..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2045,7 +2053,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bruk valgt bilde"), "usedSpace": MessageLookupByLibrary.simpleMessage("Benyttet lagringsplass"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Bekreftelse mislyktes, vennligst prøv igjen"), @@ -2054,7 +2062,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekreft"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekreft e-postadresse"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Bekreft"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Bekreft tilgangsnøkkel"), @@ -2065,8 +2073,6 @@ class MessageLookup extends MessageLookupByLibrary { "Verifiserer gjenopprettingsnøkkel..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Videoinformasjon"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Videostrømming"), "videos": MessageLookupByLibrary.simpleMessage("Videoer"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Vis aktive økter"), @@ -2093,7 +2099,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Vi støtter ikke redigering av bilder og album som du ikke eier ennå"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svakt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Velkommen tilbake!"), @@ -2102,7 +2108,7 @@ class MessageLookup extends MessageLookupByLibrary { "Betrodd kontakt kan hjelpe til med å gjenopprette dine data."), "yearShort": MessageLookupByLibrary.simpleMessage("år"), "yearly": MessageLookupByLibrary.simpleMessage("Årlig"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, avslutt"), "yesConvertToViewer": @@ -2116,7 +2122,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Ja, tilbakestill person"), "you": MessageLookupByLibrary.simpleMessage("Deg"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Du har et familieabonnement!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2135,7 +2141,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kan ikke dele med deg selv"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du har ingen arkiverte elementer."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Brukeren din har blitt slettet"), "yourMap": MessageLookupByLibrary.simpleMessage("Ditt kart"), diff --git a/mobile/lib/generated/intl/messages_or.dart b/mobile/lib/generated/intl/messages_or.dart index 2bbc1c314c..5d2261d16d 100644 --- a/mobile/lib/generated/intl/messages_or.dart +++ b/mobile/lib/generated/intl/messages_or.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'or'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index a1e5f54162..d0e37e0449 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -83,161 +83,161 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} plików, każdy po ${formattedSize}"; - static String m27(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; + static String m28(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} nie posiada konta Ente.\n\nWyślij im zaproszenie do udostępniania zdjęć."; - static String m31(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} na tym urządzeniu została bezpiecznie utworzona kopia zapasowa"; + static String m32(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} na tym urządzeniu została bezpiecznie utworzona kopia zapasowa"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} w tym albumie została bezpiecznie utworzona kopia zapasowa"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB za każdym razem, gdy ktoś zarejestruje się w płatnym planie i użyje twojego kodu"; - static String m36(endDate) => "Okres próbny ważny do ${endDate}"; + static String m37(endDate) => "Okres próbny ważny do ${endDate}"; - static String m38(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Przetwarzanie ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} element', few: '${count} elementy', many: '${count} elementów', other: '${count} elementu')}"; - static String m44(email) => + static String m45(email) => "${email} zaprosił Cię do zostania zaufanym kontaktem"; - static String m45(expiryTime) => "Link wygaśnie ${expiryTime}"; + static String m46(expiryTime) => "Link wygaśnie ${expiryTime}"; - static String m50(albumName) => "Pomyślnie przeniesiono do ${albumName}"; + static String m51(albumName) => "Pomyślnie przeniesiono do ${albumName}"; - static String m51(personName) => "Brak sugestii dla ${personName}"; + static String m52(personName) => "Brak sugestii dla ${personName}"; - static String m52(name) => "Nie ${name}?"; + static String m53(name) => "Nie ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Skontaktuj się z ${familyAdminEmail}, aby zmienić swój kod."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Siła hasła: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Porozmawiaj ze wsparciem ${providerName} jeśli zostałeś obciążony"; - static String m61(endDate) => + static String m62(endDate) => "Bezpłatny okres próbny ważny do ${endDate}.\nNastępnie możesz wybrać płatny plan."; - static String m62(toEmail) => + static String m63(toEmail) => "Prosimy o kontakt mailowy pod adresem ${toEmail}"; - static String m63(toEmail) => "Prosimy wysłać logi do ${toEmail}"; + static String m64(toEmail) => "Prosimy wysłać logi do ${toEmail}"; - static String m65(folderName) => "Przetwarzanie ${folderName}..."; + static String m66(folderName) => "Przetwarzanie ${folderName}..."; - static String m66(storeName) => "Oceń nas na ${storeName}"; + static String m67(storeName) => "Oceń nas na ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Możesz uzyskać dostęp do konta po dniu ${days} dni. Powiadomienie zostanie wysłane na ${email}."; - static String m69(email) => + static String m70(email) => "Możesz teraz odzyskać konto ${email} poprzez ustawienie nowego hasła."; - static String m70(email) => "${email} próbuje odzyskać Twoje konto."; + static String m71(email) => "${email} próbuje odzyskać Twoje konto."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Oboje otrzymujecie ${storageInGB} GB* za darmo"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} zostanie usunięty z tego udostępnionego albumu\n\nWszelkie dodane przez nich zdjęcia zostaną usunięte z albumu"; - static String m73(endDate) => "Subskrypcja odnowi się ${endDate}"; + static String m74(endDate) => "Subskrypcja odnowi się ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: 'Znaleziono ${count} wynik', few: 'Znaleziono ${count} wyniki', other: 'Znaleziono ${count} wyników')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Niezgodność długości sekcji: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "Wybrano ${count}"; + static String m78(count) => "Wybrano ${count}"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "Wybrano ${count} (twoich ${yourCount})"; - static String m79(verificationID) => + static String m81(verificationID) => "Oto mój identyfikator weryfikacyjny: ${verificationID} dla ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hej, czy możesz potwierdzić, że to jest Twój identyfikator weryfikacyjny ente.io: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Kod polecający: ${referralCode} \n\nZastosuj go w: Ustawienia → Ogólne → Polecanie, aby otrzymać ${referralStorageInGB} GB za darmo po zarejestrowaniu się w płatnym planie\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Udostępnione określonym osobom', one: 'Udostępnione 1 osobie', other: 'Udostępnione ${numberOfPeople} osobom')}"; - static String m83(emailIDs) => "Udostępnione z ${emailIDs}"; + static String m85(emailIDs) => "Udostępnione z ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Ten ${fileType} zostanie usunięty z Twojego urządzenia."; - static String m85(fileType) => + static String m87(fileType) => "Ten ${fileType} jest zarówno w Ente, jak i na twoim urządzeniu."; - static String m86(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; + static String m88(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "Użyto ${usedAmount} ${usedStorageUnit} z ${totalAmount} ${totalStorageUnit}"; - static String m91(id) => + static String m93(id) => "Twoje ${id} jest już połączony z innym kontem Ente.\nJeśli chcesz użyć swojego ${id} za pomocą tego konta, skontaktuj się z naszym wsparciem technicznym"; - static String m92(endDate) => + static String m94(endDate) => "Twoja subskrypcja zostanie anulowana dnia ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "Zachowano ${completed}/${total} wspomnień"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Naciśnij, aby przesłać, przesyłanie jest obecnie ignorowane z powodu ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Oni również otrzymują ${storageAmountInGB} GB"; - static String m96(email) => "To jest identyfikator weryfikacyjny ${email}"; + static String m98(email) => "To jest identyfikator weryfikacyjny ${email}"; - static String m99(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Wkrótce', one: '1 dzień', few: '${count} dni', other: '${count} dni')}"; - static String m102(email) => + static String m104(email) => "Zostałeś zaproszony do bycia dziedzicznym kontaktem przez ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Typ galerii ${galleryType} nie jest obsługiwany dla zmiany nazwy"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Przesyłanie jest ignorowane z powodu ${ignoreReason}"; - static String m105(count) => "Zachowywanie ${count} wspomnień..."; + static String m107(count) => "Zachowywanie ${count} wspomnień..."; - static String m106(endDate) => "Ważne do ${endDate}"; + static String m108(endDate) => "Ważne do ${endDate}"; - static String m107(email) => "Zweryfikuj ${email}"; + static String m109(email) => "Zweryfikuj ${email}"; - static String m109(email) => + static String m112(email) => "Wysłaliśmy wiadomość na adres ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} rok temu', few: '${count} lata temu', many: '${count} lat temu', other: '${count} lata temu')}"; - static String m112(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; + static String m115(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -456,6 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("Utwórz kopię zapasową wideo"), "birthday": MessageLookupByLibrary.simpleMessage("Urodziny"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Wyprzedaż z okazji Czarnego Piątku"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), @@ -528,6 +529,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Kliknij"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Kliknij na menu przepełnienia"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Zamknij"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Club według czasu przechwycenia"), @@ -770,8 +773,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Adres e-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Adres e-mail jest już zarejestrowany."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Adres e-mail nie jest zarejestrowany."), "emailVerificationToggle": @@ -854,7 +857,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuj swoje dane"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Znaleziono dodatkowe zdjęcia"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Twarz jeszcze nie zgrupowana, prosimy wrócić później"), "faceRecognition": @@ -906,8 +909,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Rodzaje plików"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Typy plików i nazwy"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Pliki usunięto"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Pliki zapisane do galerii"), @@ -923,13 +926,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Znaleziono twarze"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Bezpłatna pamięć, którą odebrano"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Darmowa pamięć użyteczna"), "freeTrial": MessageLookupByLibrary.simpleMessage("Darmowy okres próbny"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Zwolnij miejsce na urządzeniu"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -941,7 +944,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Ogólne"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generowanie kluczy szyfrujących..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Przejdź do ustawień"), "googlePlayId": @@ -955,6 +958,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Widok gościa"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Aby włączyć widok gościa, należy skonfigurować hasło urządzenia lub blokadę ekranu w ustawieniach Twojego systemu."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Nie śledzimy instalacji aplikacji. Pomogłyby nam, gdybyś powiedział/a nam, gdzie nas znalazłeś/aś!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1024,7 +1029,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Wygląda na to, że coś poszło nie tak. Spróbuj ponownie po pewnym czasie. Jeśli błąd będzie się powtarzał, skontaktuj się z naszym zespołem pomocy technicznej."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementy pokazują liczbę dni pozostałych przed trwałym usunięciem"), @@ -1054,7 +1059,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Dziedzictwo"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Odziedziczone konta"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Dziedzictwo pozwala zaufanym kontaktom na dostęp do Twojego konta w razie Twojej nieobecności."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1069,7 +1074,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Połącz adres e-mail"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktywny"), "linkExpired": MessageLookupByLibrary.simpleMessage("Wygasł"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Wygaśnięcie linku"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link wygasł"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nigdy"), @@ -1077,8 +1082,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zdjęcia Live Photo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Możesz udostępnić swoją subskrypcję swojej rodzinie"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Do tej pory zachowaliśmy ponad 30 milionów wspomnień"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Przechowujemy 3 kopie Twoich danych, jedną w podziemnym schronie"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1135,6 +1138,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Długo naciśnij element, aby wyświetlić go na pełnym ekranie"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Pętla wideo wyłączona"), "loopVideoOn": @@ -1198,7 +1203,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Przenieś do albumu"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Przenieś do ukrytego albumu"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Przeniesiono do kosza"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1213,6 +1218,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Nowy album"), "newLocation": MessageLookupByLibrary.simpleMessage("Nowa lokalizacja"), "newPerson": MessageLookupByLibrary.simpleMessage("Nowa osoba"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Nowy/a do Ente"), "newest": MessageLookupByLibrary.simpleMessage("Najnowsze"), "next": MessageLookupByLibrary.simpleMessage("Dalej"), @@ -1251,10 +1257,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Brak wyników"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nie znaleziono wyników"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nie znaleziono blokady systemowej"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nic Ci jeszcze nie udostępniono"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1264,7 +1270,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Na urządzeniu"), "onEnte": MessageLookupByLibrary.simpleMessage("W ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Tylko te"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1301,7 +1310,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Hasło zostało pomyślnie zmienione"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blokada hasłem"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Siła hasła jest obliczana, biorąc pod uwagę długość hasła, użyte znaki, i czy hasło pojawi się w 10 000 najczęściej używanych haseł"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1312,7 +1321,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Płatność się nie powiodła"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Niestety Twoja płatność nie powiodła się. Skontaktuj się z pomocą techniczną, a my Ci pomożemy!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Oczekujące elementy"), "pendingSync": @@ -1342,7 +1351,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Blokada PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Odtwórz album na telewizorze"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Subskrypcja PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1354,14 +1363,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Skontaktuj się z pomocą techniczną, jeśli problem będzie się powtarzał"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Prosimy przyznać uprawnienia"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Zaloguj się ponownie"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Prosimy wybrać szybkie linki do usunięcia"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1389,7 +1398,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Udostępnianie prywatne"), "proceed": MessageLookupByLibrary.simpleMessage("Kontynuuj"), "processed": MessageLookupByLibrary.simpleMessage("Przetworzone"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Utworzono publiczny link"), "publicLinkEnabled": @@ -1399,7 +1408,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Zgłoś"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Oceń aplikację"), "rateUs": MessageLookupByLibrary.simpleMessage("Oceń nas"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Odzyskaj"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), @@ -1408,7 +1419,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Odzyskiwanie rozpoczęte"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Klucz odzyskiwania"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1423,12 +1434,12 @@ class MessageLookup extends MessageLookupByLibrary { "Klucz odzyskiwania zweryfikowany"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Twój klucz odzyskiwania jest jedynym sposobem na odzyskanie zdjęć, jeśli zapomnisz hasła. Klucz odzyskiwania można znaleźć w Ustawieniach > Konto.\n\nWprowadź tutaj swój klucz odzyskiwania, aby sprawdzić, czy został zapisany poprawnie."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Odzyskano pomyślnie!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Zaufany kontakt próbuje uzyskać dostęp do Twojego konta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Obecne urządzenie nie jest wystarczająco wydajne, aby zweryfikować hasło, ale możemy je wygenerować w sposób działający na wszystkich urządzeniach.\n\nZaloguj się przy użyciu klucza odzyskiwania i wygeneruj nowe hasło (jeśli chcesz, możesz ponownie użyć tego samego)."), "recreatePasswordTitle": @@ -1444,7 +1455,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Przekaż ten kod swoim znajomym"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. Wykupują płatny plan"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Polecenia"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Wysyłanie poleceń jest obecnie wstrzymane"), @@ -1474,7 +1485,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Usuń link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Usuń użytkownika"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Usuń etykietę osoby"), "removePublicLink": @@ -1495,7 +1506,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Zmień nazwę pliku"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Odnów subskrypcję"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "reportBug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "resendEmail": @@ -1573,8 +1584,8 @@ class MessageLookup extends MessageLookupByLibrary { "Zaproś ludzi, a zobaczysz tutaj wszystkie udostępnione przez nich zdjęcia"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Osoby będą wyświetlane tutaj po zakończeniu przetwarzania i synchronizacji"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Bezpieczeństwo"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Zobacz publiczne linki do albumów w aplikacji"), @@ -1607,8 +1618,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Wybrane elementy zostaną usunięte ze wszystkich albumów i przeniesione do kosza."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Wyślij"), "sendEmail": MessageLookupByLibrary.simpleMessage("Wyślij e-mail"), "sendInvite": @@ -1637,16 +1648,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Udostępnij teraz album"), "shareLink": MessageLookupByLibrary.simpleMessage("Udostępnij link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Udostępnij tylko ludziom, którym chcesz"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Pobierz Ente, abyśmy mogli łatwo udostępniać zdjęcia i wideo w oryginalnej jakości\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Udostępnij użytkownikom bez konta Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Udostępnij swój pierwszy album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1659,7 +1670,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nowe udostępnione zdjęcia"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Otrzymuj powiadomienia, gdy ktoś doda zdjęcie do udostępnionego albumu, którego jesteś częścią"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Udostępnione ze mną"), "sharedWithYou": @@ -1676,11 +1687,11 @@ class MessageLookup extends MessageLookupByLibrary { "Wyloguj z pozostałych urządzeń"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Akceptuję warunki korzystania z usługi i politykę prywatności"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "To zostanie usunięte ze wszystkich albumów."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pomiń"), "social": MessageLookupByLibrary.simpleMessage("Społeczność"), "someItemsAreInBothEnteAndYourDevice": @@ -1709,6 +1720,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Przepraszamy, nie mogliśmy wygenerować bezpiecznych kluczy na tym urządzeniu.\n\nZarejestruj się z innego urządzenia."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sortuj"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sortuj według"), "sortNewestFirst": @@ -1728,13 +1741,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Pamięć"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Rodzina"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Ty"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Przekroczono limit pamięci"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Silne"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subskrybuj"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Potrzebujesz aktywnej płatnej subskrypcji, aby włączyć udostępnianie."), @@ -1751,7 +1764,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Zaproponuj funkcje"), "support": MessageLookupByLibrary.simpleMessage("Wsparcie techniczne"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronizacja zatrzymana"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronizowanie..."), @@ -1764,7 +1777,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Naciśnij, aby odblokować"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Naciśnij, aby przesłać"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Wygląda na to, że coś poszło nie tak. Spróbuj ponownie po pewnym czasie. Jeśli błąd będzie się powtarzał, skontaktuj się z naszym zespołem pomocy technicznej."), "terminate": MessageLookupByLibrary.simpleMessage("Zakończ"), @@ -1788,7 +1801,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Te elementy zostaną usunięte z Twojego urządzenia."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Zostaną one usunięte ze wszystkich albumów."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1804,7 +1817,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ten e-mail jest już używany"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Ten obraz nie posiada danych exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "To jest Twój Identyfikator Weryfikacji"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1828,11 +1841,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("ogółem"), "totalSize": MessageLookupByLibrary.simpleMessage("Całkowity rozmiar"), "trash": MessageLookupByLibrary.simpleMessage("Kosz"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Przytnij"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Zaufane kontakty"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Włącz kopię zapasową, aby automatycznie przesyłać pliki dodane do folderu urządzenia do Ente."), @@ -1852,7 +1865,7 @@ class MessageLookup extends MessageLookupByLibrary { "Pomyślnie zresetowano uwierzytelnianie dwustopniowe"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Uwierzytelnianie dwustopniowe"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Przywróć z archiwum"), "unarchiveAlbum": @@ -1877,10 +1890,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Aktualizowanie wyboru folderu..."), "upgrade": MessageLookupByLibrary.simpleMessage("Ulepsz"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Przesyłanie plików do albumu..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Zachowywanie 1 wspomnienia..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1898,7 +1911,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Użyj zaznaczone zdjęcie"), "usedSpace": MessageLookupByLibrary.simpleMessage("Zajęta przestrzeń"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Weryfikacja nie powiodła się, spróbuj ponownie"), @@ -1907,7 +1920,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Zweryfikuj adres e-mail"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Zweryfikuj klucz dostępu"), @@ -1945,7 +1958,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nie wspieramy edycji zdjęć i albumów, których jeszcze nie posiadasz"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Słabe"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Witaj ponownie!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Co nowego"), @@ -1953,7 +1966,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zaufany kontakt może pomóc w odzyskaniu Twoich danych."), "yearShort": MessageLookupByLibrary.simpleMessage("r"), "yearly": MessageLookupByLibrary.simpleMessage("Rocznie"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Tak"), "yesCancel": MessageLookupByLibrary.simpleMessage("Tak, anuluj"), "yesConvertToViewer": @@ -1985,7 +1998,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nie możesz udostępnić samemu sobie"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Nie masz żadnych zarchiwizowanych elementów."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Twoje konto zostało usunięte"), "yourMap": MessageLookupByLibrary.simpleMessage("Twoja mapa"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 3e18ebb396..b2675dfd15 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -22,21 +22,12 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Eu)"; - static String m1(count) => - "${Intl.plural(count, one: 'Adicionar colaborador', other: 'Adicionar colaboradores')}"; - - static String m2(count) => - "${Intl.plural(count, one: 'Adicionar item', other: 'Adicionar itens')}"; - static String m3(storageAmount, endDate) => - "Seu complemento ${storageAmount} é válido até ${endDate}"; - - static String m4(count) => - "${Intl.plural(count, one: 'Adicionar visualizador', other: 'Adicionar visualizadores')}"; + "Seu addon ${storageAmount} é válido até o momento ${endDate}"; static String m5(emailOrName) => "Adicionado por ${emailOrName}"; - static String m6(albumName) => "Adicionado com sucesso a ${albumName}"; + static String m6(albumName) => "Adicionado com sucesso a ${albumName}"; static String m7(name) => "Admirando ${name}"; @@ -46,15 +37,15 @@ class MessageLookup extends MessageLookupByLibrary { static String m9(versionValue) => "Versão: ${versionValue}"; static String m10(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} livre"; + "${freeAmount} ${storageUnit} grátis"; static String m11(name) => "Vistas bonitas com ${name}"; static String m12(paymentProvider) => - "Primeiramente cancele sua assinatura existente do ${paymentProvider}"; + "Por favor, cancele primeiro a sua subscrição existente de ${paymentProvider}"; static String m13(user) => - "${user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda conseguirão remover fotos existentes adicionadas por eles"; + "${user} não será capaz de adicionar mais fotos a este álbum\n\nEles ainda serão capazes de remover fotos existentes adicionadas por eles"; static String m14(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { @@ -72,278 +63,261 @@ class MessageLookup extends MessageLookupByLibrary { "Você está prestes a adicionar ${email} como contato confiável. Eles poderão recuperar sua conta se você estiver ausente por ${numOfDays} dias."; static String m18(familyAdminEmail) => - "Entre em contato com ${familyAdminEmail} para gerenciar sua assinatura"; + "Contacte ${familyAdminEmail} para gerir a sua subscrição"; static String m19(provider) => - "Entre em contato conosco em support@ente.io para gerenciar sua assinatura ${provider}."; + "Contacte-nos em support@ente.io para gerir a sua subscrição ${provider}"; - static String m20(endpoint) => "Conectado à ${endpoint}"; + static String m20(endpoint) => "Conectado a ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; + "${Intl.plural(count, one: 'Apagar ${count} item', other: 'Apagar ${count} itens')}"; static String m22(currentlyDeleting, totalCount) => - "Excluindo ${currentlyDeleting} / ${totalCount}"; + "Apagar ${currentlyDeleting} / ${totalCount}"; static String m23(albumName) => - "Isso removerá o link público para acessar \"${albumName}\"."; + "Isto removerá o link público para acessar \"${albumName}\"."; static String m24(supportEmail) => - "Envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registrado"; + "Envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registado"; static String m25(count, storageSaved) => - "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, salvando (${storageSaved}!)"; + "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, guardando (${storageSaved}!)"; static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "E-mail alterado para ${newEmail}"; + static String m28(newEmail) => "Email alterado para ${newEmail}"; - static String m28(email) => "${email} não possui uma conta Ente."; + static String m29(email) => "${email} não possui uma conta Ente."; - static String m29(email) => - "${email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos."; + static String m30(email) => + "${email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m30(name) => "Abraçando ${name}"; + static String m31(name) => "Abraçando ${name}"; - static String m31(text) => "Fotos adicionais encontradas para ${text}"; + static String m32(text) => "Fotos extras encontradas para ${text}"; - static String m32(name) => "Tendo banquete com ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + static String m33(name) => "Tendo banquete com ${name}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste álbum foi copiado com segurança"; + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; - static String m35(storageAmountInGB) => - "${storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código"; + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste álbum teve um backup seguro"; - static String m36(endDate) => "A avaliação grátis acaba em ${endDate}"; + static String m36(storageAmountInGB) => + "${storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código"; - static String m37(count) => - "Você ainda pode acessá-${Intl.plural(count, one: 'lo', other: 'los')} no Ente, contanto que você tenha uma assinatura ativa"; + static String m37(endDate) => "Teste gratuito válido até ${endDate}"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; - static String m39(count, formattedSize) => - "${Intl.plural(count, one: 'Ele pode ser excluído do dispositivo para liberar ${formattedSize}', other: 'Eles podem ser excluídos do dispositivo para liberar ${formattedSize}')}"; - - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Caminhando com ${name}"; + static String m42(name) => "Caminhando com ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m43(name) => "Últimos momentos com ${name}"; + static String m44(name) => "Últimos momentos com ${name}"; - static String m44(email) => + static String m45(email) => "${email} convidou você para ser um contato confiável"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m46(email) => "Vincular pessoa a ${email}"; + static String m47(email) => "Vincular pessoa a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Isso vinculará ${personName} a ${email}"; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'sem memórias', one: '${formattedCount} memória', other: '${formattedCount} memórias')}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m49(count) => - "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; + static String m52(personName) => "Sem sugestões para ${personName}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m53(name) => "Não é ${name}?"; - static String m51(personName) => "Sem sugestões para ${personName}"; - - static String m52(name) => "Não é ${name}?"; - - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m54(name) => "Festejando com ${name}"; + static String m55(name) => "Festejando com ${name}"; - static String m55(passwordStrengthValue) => - "Força da senha: ${passwordStrengthValue}"; + static String m56(passwordStrengthValue) => + "Força da palavra-passe: ${passwordStrengthValue}"; - static String m56(providerName) => - "Fale com o suporte ${providerName} se você foi cobrado"; + static String m57(providerName) => + "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m57(name, age) => "${name} está com ${age}!"; + static String m58(name, age) => "${name} está com ${age}!"; - static String m58(name, age) => "${name} terá ${age} em breve"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + static String m59(name, age) => "${name} terá ${age} em breve"; static String m60(count) => + "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m61(endDate) => - "Avaliação grátis válida até ${endDate}.\nVocê pode alterar para um plano pago depois."; + static String m62(endDate) => + "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m62(toEmail) => "Envie-nos um e-mail para ${toEmail}"; + static String m63(toEmail) => + "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Envie os registros para \n${toEmail}"; + static String m64(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m64(name) => "Fazendo pose com ${name}"; + static String m65(name) => "Fazendo pose com ${name}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos no ${storeName}"; + static String m67(storeName) => "Avalie-nos em ${storeName}"; - static String m67(name) => "Atribuído a ${name}"; + static String m68(name) => "Atribuído a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Você poderá acessar a conta após ${days} dias. Uma notificação será enviada para ${email}."; - static String m69(email) => + static String m70(email) => "Você pode recuperar a conta com e-mail ${email} por definir uma nova senha."; - static String m70(email) => "${email} está tentando recuperar sua conta."; + static String m71(email) => "${email} está tentando recuperar sua conta."; - static String m71(storageInGB) => - "3. Ambos os dois ganham ${storageInGB} GB* grátis"; + static String m72(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => - "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; + static String m73(userEmail) => + "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum"; - static String m73(endDate) => "Renovação de assinatura em ${endDate}"; + static String m74(endDate) => "A subscrição é renovada em ${endDate}"; - static String m74(name) => "Viajando de carro com ${name}"; + static String m75(name) => "Viajando de carro com ${name}"; - static String m113(count) => - "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; + static String m76(count) => + "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilidade de comprimento de seções: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} selecionado(s)"; + static String m78(count) => "${count} selecionado(s)"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m78(name) => "Tirando selfies com ${name}"; + static String m80(name) => "Tirando selfies com ${name}"; - static String m79(verificationID) => - "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; + static String m81(verificationID) => + "Aqui está o meu ID de verificação: ${verificationID} para ente.io."; - static String m80(verificationID) => - "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: ${verificationID}"; + static String m82(verificationID) => + "Ei, você pode confirmar que este é seu ID de verificação do ente.io: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => - "Código de referência do Ente: ${referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter ${referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io"; + static String m83(referralCode, referralStorageInGB) => + "Insira o código de referência: ${referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m83(emailIDs) => "Compartilhado com ${emailIDs}"; + static String m85(emailIDs) => "Partilhado com ${emailIDs}"; - static String m84(fileType) => - "Este ${fileType} será excluído do dispositivo."; + static String m86(fileType) => + "Este ${fileType} será eliminado do seu dispositivo."; - static String m85(fileType) => - "Este ${fileType} está no Ente e em seu dispositivo."; + static String m87(fileType) => + "Este ${fileType} encontra-se tanto no Ente como no seu dispositivo."; - static String m86(fileType) => "Este ${fileType} será excluído do Ente."; + static String m88(fileType) => "Este ${fileType} será eliminado do Ente."; - static String m87(name) => "Jogando esportes com ${name}"; + static String m89(name) => "Jogando esportes com ${name}"; - static String m88(name) => "Destacar ${name}"; + static String m90(name) => "Destacar ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m91(id) => - "Seu ${id} já está vinculado a outra conta Ente. Se você gostaria de usar seu ${id} com esta conta, entre em contato conosco\""; + static String m93(id) => + "Seu ${id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; - static String m92(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m94(endDate) => "A sua subscrição será cancelada em ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} memórias preservadas"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Toque para enviar, atualmente o envio é ignorado devido a ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m96(email) => "Este é o ID de verificação de ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; - - static String m98(dateFormat) => "${dateFormat} com o passar dos anos"; + static String m98(email) => "Este é o ID de verificação de ${email}"; static String m99(count) => - "${Intl.plural(count, zero: 'Em breve', one: '1 dia', other: '${count} dias')}"; + "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; - static String m100(year) => "Viajem em ${year}"; + static String m100(dateFormat) => "${dateFormat} com o passar dos anos"; - static String m101(location) => "Viajem à ${location}"; + static String m101(count) => + "${Intl.plural(count, zero: 'Brevemente', one: '1 dia', other: '${count} dias')}"; - static String m102(email) => + static String m102(year) => "Viajem em ${year}"; + + static String m103(location) => "Viajem à ${location}"; + + static String m104(email) => "Você foi convidado para ser um contato legado por ${email}."; - static String m103(galleryType) => - "O tipo de galeria ${galleryType} não é suportado para renomear"; + static String m105(galleryType) => + "Tipo de galeria ${galleryType} não é permitido para renomear"; - static String m104(ignoreReason) => - "O envio é ignorado devido a ${ignoreReason}"; + static String m106(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; - static String m105(count) => "Preservando ${count} memórias..."; + static String m107(count) => "Preservar ${count} memórias..."; - static String m106(endDate) => "Válido até ${endDate}"; + static String m108(endDate) => "Válido até ${endDate}"; - static String m107(email) => "Verificar ${email}"; + static String m109(email) => "Verificar e-mail"; - static String m108(count) => - "${Intl.plural(count, zero: 'Adicionado 0 visualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; + static String m112(email) => + "Enviamos um e-mail para ${email}"; - static String m109(email) => "Enviamos um e-mail à ${email}"; - - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m111(name) => "Você e ${name}"; + static String m114(name) => "Você e ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage( - "Uma nova versão do Ente está disponível."), + "Está disponível uma nova versão do Ente."), "about": MessageLookupByLibrary.simpleMessage("Sobre"), "acceptTrustInvite": MessageLookupByLibrary.simpleMessage("Aceitar convite"), "account": MessageLookupByLibrary.simpleMessage("Conta"), - "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( - "A conta já está configurada."), + "accountIsAlreadyConfigured": + MessageLookupByLibrary.simpleMessage("A conta já está ajustada."), "accountOwnerPersonAppbarTitle": m0, "accountWelcomeBack": - MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), + MessageLookupByLibrary.simpleMessage("Bem-vindo de volta!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( - "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta."), + "Eu entendo que se eu perder a minha palavra-passe, posso perder os meus dados já que esses dados são encriptados de ponta a ponta."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessões ativas"), "add": MessageLookupByLibrary.simpleMessage("Adicionar"), - "addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"), + "addAName": MessageLookupByLibrary.simpleMessage("Adiciona um nome"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Adicionar um novo e-mail"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Adicionar colaborador"), - "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Adicionar arquivos"), - "addFromDevice": - MessageLookupByLibrary.simpleMessage("Adicionar do dispositivo"), - "addItem": m2, + "addFromDevice": MessageLookupByLibrary.simpleMessage( + "Adicionar a partir do dispositivo"), "addLocation": MessageLookupByLibrary.simpleMessage("Adicionar localização"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Adicionar"), @@ -355,22 +329,21 @@ class MessageLookup extends MessageLookupByLibrary { "addNewPerson": MessageLookupByLibrary.simpleMessage("Adicionar nova pessoa"), "addOnPageSubtitle": - MessageLookupByLibrary.simpleMessage("Detalhes dos complementos"), + MessageLookupByLibrary.simpleMessage("Detalhes dos addons"), "addOnValidTill": m3, - "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), + "addOns": MessageLookupByLibrary.simpleMessage("addons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adicionar fotos"), "addSelected": - MessageLookupByLibrary.simpleMessage("Adicionar selecionado"), + MessageLookupByLibrary.simpleMessage("Adicionar selecionados"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Adicionar ao álbum"), "addToEnte": MessageLookupByLibrary.simpleMessage("Adicionar ao Ente"), "addToHiddenAlbum": - MessageLookupByLibrary.simpleMessage("Adicionar ao álbum oculto"), + MessageLookupByLibrary.simpleMessage("Adicionar a álbum oculto"), "addTrustedContact": MessageLookupByLibrary.simpleMessage("Adicionar contato confiável"), "addViewer": MessageLookupByLibrary.simpleMessage("Adicionar visualizador"), - "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Adicione suas fotos agora"), "addedAs": MessageLookupByLibrary.simpleMessage("Adicionado como"), @@ -380,13 +353,15 @@ class MessageLookup extends MessageLookupByLibrary { "Adicionando aos favoritos..."), "admiringThem": m7, "advanced": MessageLookupByLibrary.simpleMessage("Avançado"), - "advancedSettings": MessageLookupByLibrary.simpleMessage("Avançado"), - "after1Day": MessageLookupByLibrary.simpleMessage("Após 1 dia"), - "after1Hour": MessageLookupByLibrary.simpleMessage("Após 1 hora"), - "after1Month": MessageLookupByLibrary.simpleMessage("Após 1 mês"), - "after1Week": MessageLookupByLibrary.simpleMessage("Após 1 semana"), - "after1Year": MessageLookupByLibrary.simpleMessage("Após 1 ano"), - "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietário"), + "advancedSettings": + MessageLookupByLibrary.simpleMessage("Definições avançadas"), + "after1Day": MessageLookupByLibrary.simpleMessage("Depois de 1 dia"), + "after1Hour": MessageLookupByLibrary.simpleMessage("Depois de 1 Hora"), + "after1Month": MessageLookupByLibrary.simpleMessage("Depois de 1 mês"), + "after1Week": + MessageLookupByLibrary.simpleMessage("Depois de 1 semana"), + "after1Year": MessageLookupByLibrary.simpleMessage("Depois de 1 ano"), + "albumOwner": MessageLookupByLibrary.simpleMessage("Dono"), "albumParticipantsCount": m8, "albumTitle": MessageLookupByLibrary.simpleMessage("Título do álbum"), "albumUpdated": @@ -396,12 +371,12 @@ class MessageLookup extends MessageLookupByLibrary { "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage( "Todas as memórias preservadas"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( - "Todos os agrupamentos dessa pessoa serão redefinidos, e você perderá todas as sugestões feitas por essa pessoa."), + "Todos os agrupamentos para esta pessoa serão reiniciados e perderá todas as sugestões feitas para esta pessoa"), "allWillShiftRangeBasedOnFirst": MessageLookupByLibrary.simpleMessage( "Este é o primeiro do grupo. As outras fotos selecionadas serão automaticamente alteradas para esta nova data"), "allow": MessageLookupByLibrary.simpleMessage("Permitir"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( - "Permitir que as pessoas com link também adicionem fotos ao álbum compartilhado."), + "Permitir que pessoas com o link também adicionem fotos ao álbum compartilhado."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Permitir adicionar fotos"), "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( @@ -409,131 +384,126 @@ class MessageLookup extends MessageLookupByLibrary { "allowDownloads": MessageLookupByLibrary.simpleMessage("Permitir downloads"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage( - "Permitir que pessoas adicionem fotos"), - "allowPermBody": MessageLookupByLibrary.simpleMessage( - "Permita o acesso a suas fotos das Configurações para que Ente possa exibir e copiar com segurança sua biblioteca."), - "allowPermTitle": - MessageLookupByLibrary.simpleMessage("Permita acesso às Fotos"), + "Permitir que as pessoas adicionem fotos"), "androidBiometricHint": MessageLookupByLibrary.simpleMessage("Verificar identidade"), "androidBiometricNotRecognized": MessageLookupByLibrary.simpleMessage( "Não reconhecido. Tente novamente."), "androidBiometricRequiredTitle": - MessageLookupByLibrary.simpleMessage("Biométrica necessária"), + MessageLookupByLibrary.simpleMessage("Biometria necessária"), "androidBiometricSuccess": MessageLookupByLibrary.simpleMessage("Sucesso"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("Cancelar"), "androidDeviceCredentialsRequiredTitle": - MessageLookupByLibrary.simpleMessage("Credenciais necessários"), + MessageLookupByLibrary.simpleMessage( + "Credenciais do dispositivo são necessárias"), "androidDeviceCredentialsSetupDescription": - MessageLookupByLibrary.simpleMessage("Credenciais necessários"), + MessageLookupByLibrary.simpleMessage( + "Credenciais do dispositivo necessárias"), "androidGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica não está definida no dispositivo. Vá em \'Opções > Segurança\' para adicionar a autenticação biométrica."), - "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage( - "Android, iOS, Web, Computador"), + "A autenticação biométrica não está configurada no seu dispositivo. Vá a “Definições > Segurança” para adicionar a autenticação biométrica."), + "androidIosWebDesktop": + MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Autenticação necessária"), "appIcon": MessageLookupByLibrary.simpleMessage("Ícone do aplicativo"), - "appLock": - MessageLookupByLibrary.simpleMessage("Bloqueio do aplicativo"), + "appLock": MessageLookupByLibrary.simpleMessage("Bloqueio de app"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( - "Escolha entre a tela de bloqueio padrão do seu dispositivo e uma tela de bloqueio personalizada com PIN ou senha."), + "Escolha entre o ecrã de bloqueio predefinido do seu dispositivo e um ecrã de bloqueio personalizado com um PIN ou uma palavra-passe."), "appVersion": m9, "appleId": MessageLookupByLibrary.simpleMessage("ID da Apple"), "apply": MessageLookupByLibrary.simpleMessage("Aplicar"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Aplicar código"), "appstoreSubscription": - MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"), - "archive": MessageLookupByLibrary.simpleMessage("Arquivo"), + MessageLookupByLibrary.simpleMessage("Subscrição da AppStore"), + "archive": MessageLookupByLibrary.simpleMessage("............"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), - "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), + "archiving": MessageLookupByLibrary.simpleMessage("Arquivar..."), "areYouSureThatYouWantToLeaveTheFamily": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que queira sair do plano familiar?"), - "areYouSureYouWantToCancel": - MessageLookupByLibrary.simpleMessage("Deseja cancelar?"), + "Tem certeza que deseja sair do plano familiar?"), + "areYouSureYouWantToCancel": MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que quer cancelar?"), "areYouSureYouWantToChangeYourPlan": - MessageLookupByLibrary.simpleMessage("Deseja trocar de plano?"), + MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que pretende alterar o seu plano?"), "areYouSureYouWantToExit": MessageLookupByLibrary.simpleMessage( - "Tem certeza de que queira sair?"), + "Tem certeza de que deseja sair?"), "areYouSureYouWantToLogout": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que quer encerrar sessão?"), - "areYouSureYouWantToRenew": - MessageLookupByLibrary.simpleMessage("Deseja renovar?"), + "Tem certeza que deseja terminar a sessão?"), + "areYouSureYouWantToRenew": MessageLookupByLibrary.simpleMessage( + "Tem a certeza de que pretende renovar?"), "areYouSureYouWantToResetThisPerson": MessageLookupByLibrary.simpleMessage( - "Deseja redefinir esta pessoa?"), + "Tens a certeza de que queres repor esta pessoa?"), "askCancelReason": MessageLookupByLibrary.simpleMessage( - "Sua assinatura foi cancelada. Deseja compartilhar o motivo?"), + "A sua subscrição foi cancelada. Gostaria de partilhar o motivo?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( - "Por que você quer excluir sua conta?"), + "Qual o principal motivo pelo qual está a eliminar a conta?"), "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage( - "Peça que seus entes queridos compartilhem"), + "Peça aos seus entes queridos para partilharem"), "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("em um abrigo avançado"), "authToChangeEmailVerificationSetting": MessageLookupByLibrary.simpleMessage( - "Autentique-se para alterar o e-mail de verificação"), + "Por favor, autentique-se para alterar a verificação de e-mail"), "authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage( - "Autentique para alterar a configuração da tela de bloqueio"), + "Por favor, autentique-se para alterar a configuração da tela do ecrã de bloqueio"), "authToChangeYourEmail": MessageLookupByLibrary.simpleMessage( "Por favor, autentique-se para alterar o seu e-mail"), "authToChangeYourPassword": MessageLookupByLibrary.simpleMessage( - "Autentique para alterar sua senha"), + "Por favor, autentique-se para alterar a palavra-passe"), "authToConfigureTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( - "Autentique para configurar a autenticação de dois fatores"), + "Por favor, autentique para configurar a autenticação de dois fatores"), "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage( - "Autentique para iniciar a exclusão de conta"), + "Autentique-se para iniciar a eliminação da conta"), "authToManageLegacy": MessageLookupByLibrary.simpleMessage( "Autentique-se para gerenciar seus contatos confiáveis"), "authToViewPasskey": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver sua chave de acesso"), - "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver seus arquivos excluídos"), + "Autentique-se para ver a sua chave de acesso"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( - "Autentique para ver as sessões ativas"), + "Por favor, autentique-se para ver as suas sessões ativas"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( - "Autentique-se para visualizar seus arquivos ocultos"), + "Por favor, autentique para ver seus arquivos ocultos"), "authToViewYourMemories": MessageLookupByLibrary.simpleMessage( - "Autentique-se para ver suas memórias"), + "Por favor, autentique-se para ver suas memórias"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Autentique para ver sua chave de recuperação"), + "Por favor, autentique-se para ver a chave de recuperação"), "authenticating": - MessageLookupByLibrary.simpleMessage("Autenticando..."), + MessageLookupByLibrary.simpleMessage("A Autenticar..."), "authenticationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Falha na autenticação. Tente novamente"), + "Falha na autenticação, por favor tente novamente"), "authenticationSuccessful": - MessageLookupByLibrary.simpleMessage("Autenticado com sucesso!"), + MessageLookupByLibrary.simpleMessage("Autenticação bem sucedida!"), "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( - "Você verá dispositivos de transmissão disponível aqui."), + "Verá os dispositivos Cast disponíveis aqui."), "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( - "Certifique-se que as permissões da internet local estejam ligadas para o Ente Photos App, em opções."), + "Certifique-se de que as permissões de Rede local estão activadas para a aplicação Ente Photos, nas Definições."), "autoLock": MessageLookupByLibrary.simpleMessage("Bloqueio automático"), "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Tempo após o qual o aplicativo bloqueia após ser colocado em segundo plano"), + "Tempo após o qual a aplicação bloqueia depois de ser colocada em segundo plano"), "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( - "Devido ao ocorrido de erros técnicos, você foi desconectado. Pedimos desculpas pela inconveniência."), + "Devido a uma falha técnica, a sua sessão foi encerrada. Pedimos desculpas pelo incómodo."), "autoPair": - MessageLookupByLibrary.simpleMessage("Pareamento automático"), + MessageLookupByLibrary.simpleMessage("Emparelhamento automático"), "autoPairDesc": MessageLookupByLibrary.simpleMessage( - "O pareamento automático só funciona com dispositivos que suportam o Chromecast."), + "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponível"), "availableStorageSpace": m10, "backedUpFolders": MessageLookupByLibrary.simpleMessage( - "Pastas copiadas com segurança"), + "Pastas com cópia de segurança"), "backgroundWithThem": m11, "backup": MessageLookupByLibrary.simpleMessage("Cópia de segurança"), - "backupFailed": MessageLookupByLibrary.simpleMessage( - "Falhou ao copiar com segurança"), + "backupFailed": MessageLookupByLibrary.simpleMessage("Backup falhou"), "backupFile": MessageLookupByLibrary.simpleMessage( "Copiar arquivo com segurança"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage( - "Salvamento com segurança usando dados móveis"), + "Cópia de segurança através dos dados móveis"), "backupSettings": MessageLookupByLibrary.simpleMessage( - "Opções de cópia de segurança"), + "Definições da cópia de segurança"), "backupStatus": MessageLookupByLibrary.simpleMessage( "Status da cópia de segurança"), "backupStatusDescription": MessageLookupByLibrary.simpleMessage( @@ -542,41 +512,20 @@ class MessageLookup extends MessageLookupByLibrary { "Cópia de segurança de vídeos"), "beach": MessageLookupByLibrary.simpleMessage("Areia e o mar"), "birthday": MessageLookupByLibrary.simpleMessage("Aniversário"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Promoção Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Editar todas as datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites de planos familiares"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Novo Ícone"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memórias"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente."), - "cachedData": - MessageLookupByLibrary.simpleMessage("Dados armazenados em cache"), - "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), - "canNotOpenBody": MessageLookupByLibrary.simpleMessage( - "Desculpe, este álbum não pode ser aberto no aplicativo."), - "canNotOpenTitle": - MessageLookupByLibrary.simpleMessage("Não pôde abrir este álbum"), + "cachedData": MessageLookupByLibrary.simpleMessage("Dados em cache"), + "calculating": MessageLookupByLibrary.simpleMessage("Calcular..."), "canNotUploadToAlbumsOwnedByOthers": MessageLookupByLibrary.simpleMessage( - "Não é possível enviar para álbuns pertencentes a outros"), + "Não é possível fazer upload para álbuns pertencentes a outros"), "canOnlyCreateLinkForFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( - "Só é possível criar um link para arquivos pertencentes a você"), - "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( - "Só pode remover arquivos de sua propriedade"), + "Só pode criar um link para arquivos pertencentes a você"), + "canOnlyRemoveFilesOwnedByYou": + MessageLookupByLibrary.simpleMessage(""), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), "cancelAccountRecovery": MessageLookupByLibrary.simpleMessage("Cancelar recuperação"), @@ -584,69 +533,72 @@ class MessageLookup extends MessageLookupByLibrary { "Deseja mesmo cancelar a recuperação de conta?"), "cancelOtherSubscription": m12, "cancelSubscription": - MessageLookupByLibrary.simpleMessage("Cancelar assinatura"), + MessageLookupByLibrary.simpleMessage("Cancelar subscrição"), "cannotAddMorePhotosAfterBecomingViewer": m13, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( - "Não é possível excluir arquivos compartilhados"), + "Não é possível eliminar ficheiros partilhados"), "castAlbum": MessageLookupByLibrary.simpleMessage("Transferir álbum"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( - "Certifique-se de estar na mesma internet que a TV."), + "Certifique-se de estar na mesma rede que a TV."), "castIPMismatchTitle": - MessageLookupByLibrary.simpleMessage("Falhou ao transmitir álbum"), + MessageLookupByLibrary.simpleMessage("Falha ao transmitir álbum"), "castInstruction": MessageLookupByLibrary.simpleMessage( - "Acesse cast.ente.io no dispositivo desejado para parear.\n\nInsira o código abaixo para reproduzir o álbum na sua TV."), + "Visite cast.ente.io no dispositivo que pretende emparelhar.\n\n\nIntroduza o código abaixo para reproduzir o álbum na sua TV."), "centerPoint": MessageLookupByLibrary.simpleMessage("Ponto central"), "change": MessageLookupByLibrary.simpleMessage("Alterar"), "changeEmail": MessageLookupByLibrary.simpleMessage("Alterar e-mail"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( "Alterar a localização dos itens selecionados?"), - "changePassword": MessageLookupByLibrary.simpleMessage("Alterar senha"), + "changePassword": + MessageLookupByLibrary.simpleMessage("Alterar palavra-passe"), "changePasswordTitle": - MessageLookupByLibrary.simpleMessage("Alterar senha"), + MessageLookupByLibrary.simpleMessage("Alterar palavra-passe"), "changePermissions": - MessageLookupByLibrary.simpleMessage("Alterar permissões?"), + MessageLookupByLibrary.simpleMessage("Alterar permissões"), "changeYourReferralCode": MessageLookupByLibrary.simpleMessage( - "Alterar código de referência"), + "Alterar o código de referência"), "checkForUpdates": - MessageLookupByLibrary.simpleMessage("Buscar atualizações"), + MessageLookupByLibrary.simpleMessage("Procurar atualizações"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( - "Verifique sua caixa de entrada (e spam) para concluir a verificação"), - "checkStatus": MessageLookupByLibrary.simpleMessage("Verificar estado"), - "checking": MessageLookupByLibrary.simpleMessage("Verificando..."), + "Verifique a sua caixa de entrada (e spam) para concluir a verificação"), + "checkStatus": MessageLookupByLibrary.simpleMessage("Verificar status"), + "checking": MessageLookupByLibrary.simpleMessage("A verificar..."), "checkingModels": - MessageLookupByLibrary.simpleMessage("Verificando modelos..."), + MessageLookupByLibrary.simpleMessage("A verificar modelos..."), "city": MessageLookupByLibrary.simpleMessage("Na cidade"), "claimFreeStorage": MessageLookupByLibrary.simpleMessage( - "Reivindicar armazenamento grátis"), - "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), - "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), + "Solicitar armazenamento gratuito"), + "claimMore": MessageLookupByLibrary.simpleMessage("Reclamar mais!"), + "claimed": MessageLookupByLibrary.simpleMessage("Reclamado"), "claimedStorageSoFar": m14, "cleanUncategorized": - MessageLookupByLibrary.simpleMessage("Limpar não categorizado"), + MessageLookupByLibrary.simpleMessage("Limpar sem categoria"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( - "Remover todos os arquivos não categorizados que estão presentes em outros álbuns"), + "Remover todos os arquivos da Não Categorizados que estão presentes em outros álbuns"), "clearCaches": MessageLookupByLibrary.simpleMessage("Limpar cache"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Limpar índices"), - "click": MessageLookupByLibrary.simpleMessage("• Clique"), + "click": MessageLookupByLibrary.simpleMessage("Clique"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Clique no menu adicional"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Fechar"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Agrupar por tempo de captura"), - "clubByFileName": - MessageLookupByLibrary.simpleMessage("Agrupar por nome do arquivo"), + "clubByFileName": MessageLookupByLibrary.simpleMessage( + "Agrupar pelo nome de arquivo"), "clusteringProgress": MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Código aplicado"), "codeChangeLimitReached": MessageLookupByLibrary.simpleMessage( - "Desculpe, você atingiu o limite de mudanças de código."), + "Desculpe, você atingiu o limite de alterações de código."), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( - "Código copiado para a área de transferência"), + "Código copiado para área de transferência"), "codeUsedByYou": MessageLookupByLibrary.simpleMessage("Código usado por você"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( - "Crie um link para permitir que as pessoas adicionem e vejam fotos no seu álbum compartilhado sem a necessidade do aplicativo ou uma conta Ente. Ótimo para colecionar fotos de eventos."), + "Criar um link para permitir que as pessoas adicionem e visualizem fotos em seu álbum compartilhado sem precisar de um aplicativo Ente ou conta. Ótimo para coletar fotos do evento."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link colaborativo"), "collaborativeLinkCreatedFor": m15, @@ -657,10 +609,10 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsSuccessfullyAdded": m16, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": - MessageLookupByLibrary.simpleMessage("Colagem salva na galeria"), - "collect": MessageLookupByLibrary.simpleMessage("Coletar"), + MessageLookupByLibrary.simpleMessage("Colagem guardada na galeria"), + "collect": MessageLookupByLibrary.simpleMessage("Recolher"), "collectEventPhotos": - MessageLookupByLibrary.simpleMessage("Coletar fotos de evento"), + MessageLookupByLibrary.simpleMessage("Coletar fotos do evento"), "collectPhotos": MessageLookupByLibrary.simpleMessage("Coletar fotos"), "collectPhotosDescription": MessageLookupByLibrary.simpleMessage( "Crie um link onde seus amigos podem enviar fotos na qualidade original."), @@ -668,65 +620,65 @@ class MessageLookup extends MessageLookupByLibrary { "configuration": MessageLookupByLibrary.simpleMessage("Configuração"), "confirm": MessageLookupByLibrary.simpleMessage("Confirmar"), "confirm2FADisable": MessageLookupByLibrary.simpleMessage( - "Você tem certeza que queira desativar a autenticação de dois fatores?"), - "confirmAccountDeletion": - MessageLookupByLibrary.simpleMessage("Confirmar exclusão da conta"), + "Tem a certeza de que pretende desativar a autenticação de dois fatores?"), + "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( + "Confirmar eliminação de conta"), "confirmAddingTrustedContact": m17, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( - "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos."), + "Sim, pretendo apagar permanentemente esta conta e os respetivos dados em todas as aplicações."), "confirmPassword": - MessageLookupByLibrary.simpleMessage("Confirmar senha"), - "confirmPlanChange": - MessageLookupByLibrary.simpleMessage("Confirmar mudança de plano"), + MessageLookupByLibrary.simpleMessage("Confirmar palavra-passe"), + "confirmPlanChange": MessageLookupByLibrary.simpleMessage( + "Confirmar alteração de plano"), "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirmar chave de recuperação"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Confirme sua chave de recuperação"), + "Confirmar chave de recuperação"), "connectToDevice": - MessageLookupByLibrary.simpleMessage("Conectar ao dispositivo"), + MessageLookupByLibrary.simpleMessage("Ligar ao dispositivo"), "contactFamilyAdmin": m18, "contactSupport": - MessageLookupByLibrary.simpleMessage("Contatar suporte"), + MessageLookupByLibrary.simpleMessage("Contactar o suporte"), "contactToManageSubscription": m19, - "contacts": MessageLookupByLibrary.simpleMessage("Contatos"), + "contacts": MessageLookupByLibrary.simpleMessage("Contactos"), "contents": MessageLookupByLibrary.simpleMessage("Conteúdos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), - "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( - "Continuar com a avaliação grátis"), + "continueOnFreeTrial": + MessageLookupByLibrary.simpleMessage("Continuar em teste gratuito"), "convertToAlbum": MessageLookupByLibrary.simpleMessage("Converter para álbum"), "copyEmailAddress": - MessageLookupByLibrary.simpleMessage("Copiar endereço de e-mail"), + MessageLookupByLibrary.simpleMessage("Copiar endereço de email"), "copyLink": MessageLookupByLibrary.simpleMessage("Copiar link"), "copypasteThisCodentoYourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Copie e cole este código\npara o aplicativo autenticador"), + "Copie e cole este código\nno seu aplicativo de autenticação"), "couldNotBackUpTryLater": MessageLookupByLibrary.simpleMessage( - "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde."), + "Não foi possível fazer o backup de seus dados.\nTentaremos novamente mais tarde."), "couldNotFreeUpSpace": MessageLookupByLibrary.simpleMessage( - "Não foi possível liberar espaço"), + "Não foi possível libertar espaço"), "couldNotUpdateSubscription": MessageLookupByLibrary.simpleMessage( - "Não foi possível atualizar a assinatura"), + "Não foi possível atualizar a subscrição"), "count": MessageLookupByLibrary.simpleMessage("Contagem"), "crashReporting": - MessageLookupByLibrary.simpleMessage("Relatório de erros"), + MessageLookupByLibrary.simpleMessage("Relatório de falhas"), "create": MessageLookupByLibrary.simpleMessage("Criar"), "createAccount": MessageLookupByLibrary.simpleMessage("Criar conta"), "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( - "Pressione para selecionar fotos e clique em + para criar um álbum"), + "Pressione e segure para selecionar fotos e clique em + para criar um álbum"), "createCollaborativeLink": MessageLookupByLibrary.simpleMessage("Criar link colaborativo"), - "createCollage": MessageLookupByLibrary.simpleMessage("Criar colagem"), + "createCollage": MessageLookupByLibrary.simpleMessage("Criar coleção"), "createNewAccount": MessageLookupByLibrary.simpleMessage("Criar nova conta"), "createOrSelectAlbum": MessageLookupByLibrary.simpleMessage("Criar ou selecionar álbum"), "createPublicLink": MessageLookupByLibrary.simpleMessage("Criar link público"), - "creatingLink": MessageLookupByLibrary.simpleMessage("Criando link..."), + "creatingLink": MessageLookupByLibrary.simpleMessage("Criar link..."), "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Atualização crítica disponível"), - "crop": MessageLookupByLibrary.simpleMessage("Cortar"), + "crop": MessageLookupByLibrary.simpleMessage("Recortar"), "curatedMemories": MessageLookupByLibrary.simpleMessage("Curated memories"), "currentUsageIs": @@ -740,92 +692,90 @@ class MessageLookup extends MessageLookupByLibrary { "dayYesterday": MessageLookupByLibrary.simpleMessage("Ontem"), "declineTrustInvite": MessageLookupByLibrary.simpleMessage("Recusar convite"), - "decrypting": - MessageLookupByLibrary.simpleMessage("Descriptografando..."), + "decrypting": MessageLookupByLibrary.simpleMessage("A desencriptar…"), "decryptingVideo": MessageLookupByLibrary.simpleMessage("Descriptografando vídeo..."), "deduplicateFiles": MessageLookupByLibrary.simpleMessage("Arquivos duplicados"), - "delete": MessageLookupByLibrary.simpleMessage("Excluir"), - "deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"), + "delete": MessageLookupByLibrary.simpleMessage("Apagar"), + "deleteAccount": MessageLookupByLibrary.simpleMessage("Eliminar conta"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( - "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar."), + "Lamentamos a sua partida. Indique-nos a razão para podermos melhorar o serviço."), "deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage( "Excluir conta permanentemente"), - "deleteAlbum": MessageLookupByLibrary.simpleMessage("Excluir álbum"), + "deleteAlbum": MessageLookupByLibrary.simpleMessage("Apagar álbum"), "deleteAlbumDialog": MessageLookupByLibrary.simpleMessage( - "Também excluir as fotos (e vídeos) presentes neste álbum de todos os outros álbuns que eles fazem parte?"), + "Eliminar também as fotos (e vídeos) presentes neste álbum de all os outros álbuns de que fazem parte?"), "deleteAlbumsDialogBody": MessageLookupByLibrary.simpleMessage( - "Isso excluirá todos os álbuns vazios. Isso é útil quando você quiser reduzir a desordem no seu álbum."), - "deleteAll": MessageLookupByLibrary.simpleMessage("Excluir tudo"), + "Esta ação elimina todos os álbuns vazios. Isto é útil quando pretende reduzir a confusão na sua lista de álbuns."), + "deleteAll": MessageLookupByLibrary.simpleMessage("Apagar tudo"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( - "Esta conta está vinculada aos outros aplicativos do Ente, se você usar algum. Seus dados baixados, entre todos os aplicativos do Ente, serão programados para exclusão, e sua conta será permanentemente excluída."), + "Esta conta está ligada a outras aplicações Ente, se utilizar alguma. Os seus dados carregados, em todas as aplicações Ente, serão agendados para eliminação e a sua conta será permanentemente eliminada."), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( - "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado."), + "Envie um e-mail para accountt-deletion@ente.io a partir do seu endereço de email registrado."), "deleteEmptyAlbums": - MessageLookupByLibrary.simpleMessage("Excluir álbuns vazios"), + MessageLookupByLibrary.simpleMessage("Apagar álbuns vazios"), "deleteEmptyAlbumsWithQuestionMark": - MessageLookupByLibrary.simpleMessage("Excluir álbuns vazios?"), + MessageLookupByLibrary.simpleMessage("Apagar álbuns vazios?"), "deleteFromBoth": - MessageLookupByLibrary.simpleMessage("Excluir de ambos"), + MessageLookupByLibrary.simpleMessage("Apagar de ambos"), "deleteFromDevice": - MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), + MessageLookupByLibrary.simpleMessage("Apagar do dispositivo"), "deleteFromEnte": - MessageLookupByLibrary.simpleMessage("Excluir do Ente"), + MessageLookupByLibrary.simpleMessage("Apagar do Ente"), "deleteItemCount": m21, "deleteLocation": - MessageLookupByLibrary.simpleMessage("Excluir localização"), - "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), + MessageLookupByLibrary.simpleMessage("Apagar localização"), + "deletePhotos": MessageLookupByLibrary.simpleMessage("Apagar fotos"), "deleteProgress": m22, "deleteReason1": MessageLookupByLibrary.simpleMessage( - "Está faltando um recurso-chave que eu preciso"), + "Falta uma funcionalidade-chave de que eu necessito"), "deleteReason2": MessageLookupByLibrary.simpleMessage( - "O aplicativo ou um certo recurso não funciona da maneira que eu acredito que deveria funcionar"), + "O aplicativo ou um determinado recurso não se comportou como era suposto"), "deleteReason3": MessageLookupByLibrary.simpleMessage( - "Encontrei outro serviço que considero melhor"), + "Encontrei outro serviço de que gosto mais"), "deleteReason4": - MessageLookupByLibrary.simpleMessage("Meu motivo não está listado"), + MessageLookupByLibrary.simpleMessage("O motivo não está na lista"), "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( - "Sua solicitação será revisada em até 72 horas."), + "O seu pedido será processado dentro de 72 horas."), "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage( "Excluir álbum compartilhado?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( - "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros"), - "deselectAll": - MessageLookupByLibrary.simpleMessage("Deselecionar tudo"), + "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que são propriedade de outros"), + "deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar tudo"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("Feito para ter longevidade"), "details": MessageLookupByLibrary.simpleMessage("Detalhes"), "developerSettings": - MessageLookupByLibrary.simpleMessage("Opções de desenvolvedor"), + MessageLookupByLibrary.simpleMessage("Definições do programador"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( - "Deseja modificar as Opções de Desenvolvedor?"), + "Tem a certeza de que pretende modificar as definições de programador?"), "deviceCodeHint": - MessageLookupByLibrary.simpleMessage("Insira o código"), + MessageLookupByLibrary.simpleMessage("Introduza o código"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( - "Arquivos adicionados ao álbum do dispositivo serão automaticamente enviados para o Ente."), + "Os ficheiros adicionados a este álbum de dispositivo serão automaticamente transferidos para o Ente."), "deviceLock": MessageLookupByLibrary.simpleMessage("Bloqueio do dispositivo"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( - "Desativa o bloqueio de tela quando o Ente está de fundo e têm uma cópia de segurança sendo feita. Isso normalmente não é necessário, no entanto, ajuda a envios grandes e importações iniciais de bibliotecas maiores concluírem mais rápido."), + "Desativar o bloqueio do ecrã do dispositivo quando o Ente estiver em primeiro plano e houver uma cópia de segurança em curso. Normalmente, isto não é necessário, mas pode ajudar a que os grandes carregamentos e as importações iniciais de grandes bibliotecas sejam concluídos mais rapidamente."), "deviceNotFound": MessageLookupByLibrary.simpleMessage("Dispositivo não encontrado"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Você sabia?"), "disableAutoLock": MessageLookupByLibrary.simpleMessage( "Desativar bloqueio automático"), "disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage( - "Os visualizadores podem fazer capturas de tela ou salvar uma cópia de suas fotos usando ferramentas externas"), + "Visualizadores ainda podem fazer capturas de tela ou salvar uma cópia das suas fotos usando ferramentas externas"), "disableDownloadWarningTitle": - MessageLookupByLibrary.simpleMessage("Por favor, saiba que"), + MessageLookupByLibrary.simpleMessage("Por favor, observe"), "disableLinkMessage": m23, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Desativar autenticação de dois fatores"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( - "Desativando a autenticação de dois fatores..."), + "Desativar a autenticação de dois factores..."), "discord": MessageLookupByLibrary.simpleMessage("Discord"), - "discover": MessageLookupByLibrary.simpleMessage("Explorar"), - "discover_babies": MessageLookupByLibrary.simpleMessage("Bebês"), + "discover": MessageLookupByLibrary.simpleMessage("Descobrir"), + "discover_babies": MessageLookupByLibrary.simpleMessage("Bebés"), "discover_celebrations": MessageLookupByLibrary.simpleMessage("Comemorações"), "discover_food": MessageLookupByLibrary.simpleMessage("Comida"), @@ -838,29 +788,30 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Animais de estimação"), "discover_receipts": MessageLookupByLibrary.simpleMessage("Recibos"), "discover_screenshots": - MessageLookupByLibrary.simpleMessage("Capturas de tela"), + MessageLookupByLibrary.simpleMessage("Capturas de ecrã"), "discover_selfies": MessageLookupByLibrary.simpleMessage("Selfies"), "discover_sunset": MessageLookupByLibrary.simpleMessage("Pôr do sol"), "discover_visiting_cards": MessageLookupByLibrary.simpleMessage("Cartões de visita"), "discover_wallpapers": MessageLookupByLibrary.simpleMessage("Papéis de parede"), - "dismiss": MessageLookupByLibrary.simpleMessage("Descartar"), + "dismiss": MessageLookupByLibrary.simpleMessage("Rejeitar"), "distanceInKMUnit": MessageLookupByLibrary.simpleMessage("km"), - "doNotSignOut": MessageLookupByLibrary.simpleMessage("Não sair"), + "doNotSignOut": + MessageLookupByLibrary.simpleMessage("Não terminar a sessão"), "doThisLater": - MessageLookupByLibrary.simpleMessage("Fazer isso depois"), + MessageLookupByLibrary.simpleMessage("Fazer isto mais tarde"), "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage( - "Você quer descartar as edições que você fez?"), + "Pretende eliminar as edições que efectuou?"), "done": MessageLookupByLibrary.simpleMessage("Concluído"), "dontSave": MessageLookupByLibrary.simpleMessage("Não salvar"), - "doubleYourStorage": - MessageLookupByLibrary.simpleMessage("Duplique seu armazenamento"), - "download": MessageLookupByLibrary.simpleMessage("Baixar"), + "doubleYourStorage": MessageLookupByLibrary.simpleMessage( + "Duplicar o seu armazenamento"), + "download": MessageLookupByLibrary.simpleMessage("Download"), "downloadFailed": - MessageLookupByLibrary.simpleMessage("Falhou ao baixar"), - "downloading": MessageLookupByLibrary.simpleMessage("Baixando..."), + MessageLookupByLibrary.simpleMessage("Falha no download"), + "downloading": MessageLookupByLibrary.simpleMessage("A transferir..."), "dropSupportEmail": m24, "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, @@ -871,103 +822,97 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Editar localização"), "editPerson": MessageLookupByLibrary.simpleMessage("Editar pessoa"), "editTime": MessageLookupByLibrary.simpleMessage("Editar tempo"), - "editsSaved": MessageLookupByLibrary.simpleMessage("Edições salvas"), + "editsSaved": MessageLookupByLibrary.simpleMessage("Edição guardada"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( - "Edições à localização serão apenas vistos no Ente"), + "Edições para localização só serão vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), - "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailAlreadyRegistered": - MessageLookupByLibrary.simpleMessage("E-mail já registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, - "emailNotRegistered": - MessageLookupByLibrary.simpleMessage("E-mail não registrado."), + "email": MessageLookupByLibrary.simpleMessage("Email"), + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": - MessageLookupByLibrary.simpleMessage("Enviar registros por e-mail"), - "embracingThem": m30, + MessageLookupByLibrary.simpleMessage("Enviar logs por e-mail"), + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contatos de emergência"), "empty": MessageLookupByLibrary.simpleMessage("Esvaziar"), - "emptyTrash": - MessageLookupByLibrary.simpleMessage("Esvaziar a lixeira?"), + "emptyTrash": MessageLookupByLibrary.simpleMessage("Esvaziar lixo?"), "enable": MessageLookupByLibrary.simpleMessage("Ativar"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( - "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados"), + "O Ente suporta a aprendizagem automática no dispositivo para reconhecimento facial, pesquisa mágica e outras funcionalidades de pesquisa avançadas"), "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( - "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial"), + "Habilitar aprendizagem automática para pesquisa mágica e reconhecimento de rosto"), "enableMaps": MessageLookupByLibrary.simpleMessage("Ativar mapas"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( - "Isso exibirá suas fotos em um mapa mundial.\n\nEste mapa é hospedado por Open Street Map, e as exatas localizações das fotos nunca serão compartilhadas.\n\nVocê pode desativar esta função a qualquer momento em Opções."), + "Esta opção mostra as suas fotografias num mapa do mundo.\n\n\nEste mapa é alojado pelo Open Street Map e as localizações exactas das suas fotografias nunca são partilhadas.\n\n\nPode desativar esta funcionalidade em qualquer altura nas Definições."), "enabled": MessageLookupByLibrary.simpleMessage("Ativado"), - "encryptingBackup": MessageLookupByLibrary.simpleMessage( - "Criptografando cópia de segurança..."), - "encryption": MessageLookupByLibrary.simpleMessage("Criptografia"), + "encryptingBackup": + MessageLookupByLibrary.simpleMessage("Criptografando backup..."), + "encryption": MessageLookupByLibrary.simpleMessage("Encriptação"), "encryptionKeys": - MessageLookupByLibrary.simpleMessage("Chaves de criptografia"), + MessageLookupByLibrary.simpleMessage("Chaves de encriptação"), "endpointUpdatedMessage": MessageLookupByLibrary.simpleMessage( - "Ponto final atualizado com sucesso"), + "Endpoint atualizado com sucesso"), "endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage( - "Criptografado de ponta a ponta por padrão"), + "Criptografia de ponta a ponta por padrão"), "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": MessageLookupByLibrary.simpleMessage( "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles"), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "Ente precisa de permissão para preservar suas fotos"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( - "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo."), + "O Ente preserva as suas memórias, para que estejam sempre disponíveis, mesmo que perca o seu dispositivo."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( "Sua família também pode ser adicionada ao seu plano."), "enterAlbumName": - MessageLookupByLibrary.simpleMessage("Inserir nome do álbum"), + MessageLookupByLibrary.simpleMessage("Introduzir nome do álbum"), "enterCode": MessageLookupByLibrary.simpleMessage("Insira o código"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( - "Insira o código fornecido pelo seu amigo para reivindicar o armazenamento grátis para os dois"), + "Introduza o código fornecido pelo seu amigo para obter armazenamento gratuito para ambos"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("Aniversário (opcional)"), - "enterEmail": MessageLookupByLibrary.simpleMessage("Inserir e-mail"), + "enterEmail": MessageLookupByLibrary.simpleMessage("Digite o e-mail"), "enterFileName": MessageLookupByLibrary.simpleMessage("Inserir nome do arquivo"), "enterName": MessageLookupByLibrary.simpleMessage("Inserir nome"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( - "Insira uma senha nova para criptografar seus dados"), - "enterPassword": MessageLookupByLibrary.simpleMessage("Inserir senha"), + "Inserir uma nova palavra-passe para encriptar os seus dados"), + "enterPassword": + MessageLookupByLibrary.simpleMessage("Introduzir palavra-passe"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( - "Insira uma senha que podemos usar para criptografar seus dados"), + "Inserir uma palavra-passe para encriptar os seus dados"), "enterPersonName": MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"), - "enterPin": MessageLookupByLibrary.simpleMessage("Inserir PIN"), + "enterPin": MessageLookupByLibrary.simpleMessage("Introduzir PIN"), "enterReferralCode": MessageLookupByLibrary.simpleMessage( - "Inserir código de referência"), + "Insira o código de referência"), "enterThe6digitCodeFromnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Digite o código de 6 dígitos do\naplicativo de autenticador"), + "Introduzir o código de 6 dígitos da\nsua aplicação de autenticação"), "enterValidEmail": MessageLookupByLibrary.simpleMessage( - "Insira um endereço de e-mail válido."), + "Por favor, insira um endereço de email válido."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( - "Insira seu endereço de e-mail"), - "enterYourPassword": - MessageLookupByLibrary.simpleMessage("Insira sua senha"), + "Insira o seu endereço de email"), + "enterYourPassword": MessageLookupByLibrary.simpleMessage( + "Introduza a sua palavra-passe"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( - "Insira sua chave de recuperação"), + "Insira a sua chave de recuperação"), "error": MessageLookupByLibrary.simpleMessage("Erro"), - "everywhere": - MessageLookupByLibrary.simpleMessage("em todas as partes"), + "everywhere": MessageLookupByLibrary.simpleMessage("em todo o lado"), "exif": MessageLookupByLibrary.simpleMessage("EXIF"), "existingUser": - MessageLookupByLibrary.simpleMessage("Usuário existente"), + MessageLookupByLibrary.simpleMessage("Utilizador existente"), "expiredLinkInfo": MessageLookupByLibrary.simpleMessage( - "O link expirou. Selecione um novo tempo de expiração ou desative a expiração do link."), - "exportLogs": - MessageLookupByLibrary.simpleMessage("Exportar registros"), + "Este link expirou. Por favor, selecione um novo tempo de expiração ou desabilite a expiração do link."), + "exportLogs": MessageLookupByLibrary.simpleMessage("Exportar logs"), "exportYourData": - MessageLookupByLibrary.simpleMessage("Exportar dados"), + MessageLookupByLibrary.simpleMessage("Exportar os seus dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Rosto não agrupado ainda, volte aqui mais tarde"), "faceRecognition": @@ -975,142 +920,143 @@ class MessageLookup extends MessageLookupByLibrary { "faces": MessageLookupByLibrary.simpleMessage("Rostos"), "failed": MessageLookupByLibrary.simpleMessage("Falhou"), "failedToApplyCode": - MessageLookupByLibrary.simpleMessage("Falhou ao aplicar código"), + MessageLookupByLibrary.simpleMessage("Falha ao aplicar código"), "failedToCancel": MessageLookupByLibrary.simpleMessage("Falhou ao cancelar"), - "failedToDownloadVideo": - MessageLookupByLibrary.simpleMessage("Falhou ao baixar vídeo"), + "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage( + "Falha ao fazer o download do vídeo"), "failedToFetchActiveSessions": MessageLookupByLibrary.simpleMessage( - "Falhou ao obter sessões ativas"), + "Falha ao obter sessões em atividade"), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage( - "Falhou ao obter original para edição"), + "Falha ao obter original para edição"), "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( - "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde."), + "Não foi possível obter detalhes de indicação. Por favor, tente novamente mais tarde."), "failedToLoadAlbums": - MessageLookupByLibrary.simpleMessage("Falhou ao carregar álbuns"), - "failedToPlayVideo": - MessageLookupByLibrary.simpleMessage("Falhou ao reproduzir vídeo"), + MessageLookupByLibrary.simpleMessage("Falha ao carregar álbuns"), + "failedToPlayVideo": MessageLookupByLibrary.simpleMessage( + "Falha ao reproduzir multimédia"), "failedToRefreshStripeSubscription": MessageLookupByLibrary.simpleMessage( - "Falhou ao atualizar assinatura"), + "Falha ao atualizar subscrição"), "failedToRenew": MessageLookupByLibrary.simpleMessage("Falhou ao renovar"), "failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage( - "Falhou ao verificar estado do pagamento"), + "Falha ao verificar status do pagamento"), "familyPlanOverview": MessageLookupByLibrary.simpleMessage( - "Adicione 5 familiares para seu plano existente sem pagar nenhum custo adicional.\n\nCada membro ganha seu espaço privado, significando que eles não podem ver os arquivos dos outros a menos que eles sejam compartilhados.\n\nOs planos familiares estão disponíveis para clientes que já tem uma assinatura paga do Ente.\n\nAssine agora para iniciar!"), + "Adicione 5 membros da família ao seu plano existente sem pagar mais.\n\n\nCada membro tem o seu próprio espaço privado e não pode ver os ficheiros dos outros, a menos que sejam partilhados.\n\n\nOs planos familiares estão disponíveis para clientes que tenham uma subscrição paga do Ente.\n\n\nSubscreva agora para começar!"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("Família"), "familyPlans": MessageLookupByLibrary.simpleMessage("Planos familiares"), - "faq": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), + "faq": MessageLookupByLibrary.simpleMessage("Perguntas Frequentes"), "faqs": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, - "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), + "feastingWithThem": m33, + "feedback": MessageLookupByLibrary.simpleMessage("Opinião"), "file": MessageLookupByLibrary.simpleMessage("Arquivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( - "Falhou ao salvar arquivo na galeria"), + "Falha ao guardar o ficheiro na galeria"), "fileInfoAddDescHint": - MessageLookupByLibrary.simpleMessage("Adicionar descrição..."), + MessageLookupByLibrary.simpleMessage("Acrescente uma descrição..."), "fileNotUploadedYet": MessageLookupByLibrary.simpleMessage("Arquivo ainda não enviado"), "fileSavedToGallery": - MessageLookupByLibrary.simpleMessage("Arquivo salvo na galeria"), + MessageLookupByLibrary.simpleMessage("Arquivo guardado na galeria"), "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": - MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), - "filesSavedToGallery": - MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"), + MessageLookupByLibrary.simpleMessage("Arquivos apagados"), + "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( + "Arquivos guardados na galeria"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Busque pessoas facilmente pelo nome"), + "Encontrar pessoas rapidamente pelo nome"), "findThemQuickly": - MessageLookupByLibrary.simpleMessage("Busque-os rapidamente"), + MessageLookupByLibrary.simpleMessage("Ache-os rapidamente"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"), "food": MessageLookupByLibrary.simpleMessage("Prazer em culinária"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para suas memórias"), - "forgotPassword": - MessageLookupByLibrary.simpleMessage("Esqueci a senha"), + "forgotPassword": MessageLookupByLibrary.simpleMessage( + "Esqueceu-se da palavra-passe"), "foundFaces": MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( - "Armazenamento grátis reivindicado"), - "freeStorageOnReferralSuccess": m35, - "freeStorageUsable": - MessageLookupByLibrary.simpleMessage("Armazenamento disponível"), - "freeTrial": MessageLookupByLibrary.simpleMessage("Avaliação grátis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "Armazenamento gratuito reclamado"), + "freeStorageOnReferralSuccess": m36, + "freeStorageUsable": MessageLookupByLibrary.simpleMessage( + "Armazenamento livre utilizável"), + "freeTrial": MessageLookupByLibrary.simpleMessage("Teste grátis"), + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( - "Liberar espaço no dispositivo"), + "Libertar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( - "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança."), - "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espaço"), - "freeUpSpaceSaving": m39, + "Poupe espaço no seu dispositivo limpando ficheiros dos quais já foi feita uma cópia de segurança."), + "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libertar espaço"), "gallery": MessageLookupByLibrary.simpleMessage("Galeria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( - "Até 1.000 memórias exibidas na galeria"), + "Até 1000 memórias mostradas na galeria"), "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( - "Gerando chaves de criptografia..."), - "genericProgress": m40, - "goToSettings": MessageLookupByLibrary.simpleMessage("Ir às opções"), + "Gerando chaves de encriptação..."), + "genericProgress": m41, + "goToSettings": + MessageLookupByLibrary.simpleMessage("Ir para as definições"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID do Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( - "Permita o acesso a todas as fotos nas opções do aplicativo"), + "Por favor, permita o acesso a todas as fotos nas definições do aplicativo"), "grantPermission": - MessageLookupByLibrary.simpleMessage("Conceder permissões"), + MessageLookupByLibrary.simpleMessage("Conceder permissão"), "greenery": MessageLookupByLibrary.simpleMessage("A vegetação verde"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage("Agrupar fotos próximas"), - "guestView": MessageLookupByLibrary.simpleMessage("Vista do convidado"), + "guestView": MessageLookupByLibrary.simpleMessage("Visão de convidado"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( - "Para ativar a vista do convidado, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), + "Para ativar a vista de convidado, configure o código de acesso do dispositivo ou o bloqueio do ecrã nas definições do sistema."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( - "Não rastreamos instalações de aplicativo. Seria útil se você contasse onde nos encontrou!"), + "Não monitorizamos as instalações de aplicações. Ajudaria se nos dissesse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( - "Como você soube do Ente? (opcional)"), + "Como é que soube do Ente? (opcional)"), "help": MessageLookupByLibrary.simpleMessage("Ajuda"), "hidden": MessageLookupByLibrary.simpleMessage("Oculto"), "hide": MessageLookupByLibrary.simpleMessage("Ocultar"), "hideContent": MessageLookupByLibrary.simpleMessage("Ocultar conteúdo"), "hideContentDescriptionAndroid": MessageLookupByLibrary.simpleMessage( - "Oculta os conteúdos do aplicativo no seletor de aplicativos e desativa capturas de tela"), + "Oculta o conteúdo da aplicação no alternador de aplicações e desactiva as capturas de ecrã"), "hideContentDescriptionIos": MessageLookupByLibrary.simpleMessage( - "Oculta o conteúdo no seletor de aplicativos"), + "Oculta o conteúdo da aplicação no alternador de aplicações"), "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar itens compartilhados da galeria inicial"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": - MessageLookupByLibrary.simpleMessage("Hospedado em OSM France"), + MessageLookupByLibrary.simpleMessage("Hospedado na OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Como funciona"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( - "Peça-os para manterem pressionado no endereço de e-mail na tela de opções, e verifique-se os IDs de ambos os dispositivos correspondem."), + "Por favor, peça-lhes para pressionar longamente o endereço de e-mail na tela de configurações e verifique se os IDs de ambos os dispositivos coincidem."), "iOSGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica não está definida no dispositivo. Ative o Touch ID ou Face ID no dispositivo."), + "A autenticação biométrica não está configurada no seu dispositivo. Active o Touch ID ou o Face ID no seu telemóvel."), "iOSLockOut": MessageLookupByLibrary.simpleMessage( - "A autenticação biométrica está desativada. Bloqueie e desbloqueie sua tela para ativá-la."), + "A autenticação biométrica está desativada. Por favor, bloqueie e desbloqueie o ecrã para ativá-la."), "iOSOkButton": MessageLookupByLibrary.simpleMessage("OK"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignorar"), "ignored": MessageLookupByLibrary.simpleMessage("ignorado"), "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( - "Alguns arquivos neste álbum são ignorados do envio porque eles foram anteriormente excluídos do Ente."), + "Alguns ficheiros deste álbum não podem ser carregados porque foram anteriormente eliminados do Ente."), "imageNotAnalyzed": MessageLookupByLibrary.simpleMessage("Imagem não analisada"), "immediately": MessageLookupByLibrary.simpleMessage("Imediatamente"), - "importing": MessageLookupByLibrary.simpleMessage("Importando...."), + "importing": MessageLookupByLibrary.simpleMessage("A importar..."), "incorrectCode": - MessageLookupByLibrary.simpleMessage("Código incorreto"), + MessageLookupByLibrary.simpleMessage("Código incorrecto"), "incorrectPasswordTitle": - MessageLookupByLibrary.simpleMessage("Senha incorreta"), + MessageLookupByLibrary.simpleMessage("Palavra-passe incorreta"), "incorrectRecoveryKey": MessageLookupByLibrary.simpleMessage( "Chave de recuperação incorreta"), "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage( @@ -1119,7 +1065,7 @@ class MessageLookup extends MessageLookupByLibrary { "Chave de recuperação incorreta"), "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "A indexação parou, ela será retomada automaticamente quando o dispositivo estiver pronto."), + "A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto."), "ineligible": MessageLookupByLibrary.simpleMessage("Inelegível"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": @@ -1127,28 +1073,28 @@ class MessageLookup extends MessageLookupByLibrary { "installManually": MessageLookupByLibrary.simpleMessage("Instalar manualmente"), "invalidEmailAddress": - MessageLookupByLibrary.simpleMessage("Endereço de e-mail inválido"), + MessageLookupByLibrary.simpleMessage("Endereço de email inválido"), "invalidEndpoint": - MessageLookupByLibrary.simpleMessage("Ponto final inválido"), + MessageLookupByLibrary.simpleMessage("Endpoint inválido"), "invalidEndpointMessage": MessageLookupByLibrary.simpleMessage( - "Desculpe, o ponto final inserido é inválido. Insira um ponto final válido e tente novamente."), + "Desculpe, o endpoint que introduziu é inválido. Introduza um ponto final válido e tente novamente."), "invalidKey": MessageLookupByLibrary.simpleMessage("Chave inválida"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( - "A chave de recuperação que você inseriu não é válida. Certifique-se de conter 24 caracteres, e verifique a ortografia de cada um deles.\n\nSe você inseriu um código de recuperação mais antigo, verifique se ele tem 64 caracteres e verifique cada um deles."), + "A chave de recuperação que inseriu não é válida. Por favor, certifique-se que ela contém 24 palavras e verifique a ortografia de cada uma.\n\nSe inseriu um código de recuperação mais antigo, certifique-se de que tem 64 caracteres e verifique cada um deles."), "invite": MessageLookupByLibrary.simpleMessage("Convidar"), "inviteToEnte": - MessageLookupByLibrary.simpleMessage("Convidar ao Ente"), + MessageLookupByLibrary.simpleMessage("Convidar para Ente"), "inviteYourFriends": - MessageLookupByLibrary.simpleMessage("Convide seus amigos"), - "inviteYourFriendsToEnte": - MessageLookupByLibrary.simpleMessage("Convide seus amigos ao Ente"), + MessageLookupByLibrary.simpleMessage("Convide os seus amigos"), + "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( + "Convide seus amigos para o Ente"), "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( - "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "itemCount": m42, + "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio."), + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( - "Os itens exibem o número de dias restantes antes da exclusão permanente"), + "Os itens mostram o número de dias restantes antes da eliminação permanente"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos deste álbum"), "join": MessageLookupByLibrary.simpleMessage("Unir-se"), @@ -1160,13 +1106,13 @@ class MessageLookup extends MessageLookupByLibrary { "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( "para adicionar isso aos álbuns compartilhados"), "joinDiscord": - MessageLookupByLibrary.simpleMessage("Junte-se ao Discord"), + MessageLookupByLibrary.simpleMessage("Juntar-se ao Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Manter fotos"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( - "Ajude-nos com esta informação"), + "Por favor, ajude-nos com esta informação"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última atualização"), "lastYearsTrip": @@ -1174,57 +1120,56 @@ class MessageLookup extends MessageLookupByLibrary { "leave": MessageLookupByLibrary.simpleMessage("Sair"), "leaveAlbum": MessageLookupByLibrary.simpleMessage("Sair do álbum"), "leaveFamily": - MessageLookupByLibrary.simpleMessage("Sair do plano familiar"), + MessageLookupByLibrary.simpleMessage("Deixar plano famíliar"), "leaveSharedAlbum": MessageLookupByLibrary.simpleMessage( "Sair do álbum compartilhado?"), "left": MessageLookupByLibrary.simpleMessage("Esquerda"), "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Contas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "O legado permite que contatos confiáveis acessem sua conta em sua ausência."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( "Contatos confiáveis podem iniciar recuperação de conta. Se não cancelado dentro de 30 dias, redefina sua senha e acesse sua conta."), - "light": MessageLookupByLibrary.simpleMessage("Brilho"), + "light": MessageLookupByLibrary.simpleMessage("Claro"), "lightTheme": MessageLookupByLibrary.simpleMessage("Claro"), "link": MessageLookupByLibrary.simpleMessage("Vincular"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Link copiado para a área de transferência"), "linkDeviceLimit": - MessageLookupByLibrary.simpleMessage("Limite do dispositivo"), + MessageLookupByLibrary.simpleMessage("Limite de dispositivo"), "linkEmail": MessageLookupByLibrary.simpleMessage("Vincular e-mail"), "linkEmailToContactBannerCaption": MessageLookupByLibrary.simpleMessage("para compartilhar rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, - "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), + "linkExpiresOn": m46, + "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expirado"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nunca"), "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular pessoa"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "para melhor experiência de compartilhamento"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, - "livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, + "livePhotos": + MessageLookupByLibrary.simpleMessage("Fotos Em Tempo Real"), "loadMessage1": MessageLookupByLibrary.simpleMessage( - "Você pode compartilhar sua assinatura com seus familiares"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Nós preservamos mais de 30 milhões de memórias até então"), + "Pode partilhar a sua subscrição com a sua família"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( "Todos os nossos aplicativos são de código aberto"), "loadMessage5": MessageLookupByLibrary.simpleMessage( "Nosso código-fonte e criptografia foram auditadas externamente"), - "loadMessage6": MessageLookupByLibrary.simpleMessage( - "Você pode compartilhar links para seus álbuns com seus entes queridos"), + "loadMessage6": + MessageLookupByLibrary.simpleMessage("Deixar o álbum partilhado?"), "loadMessage7": MessageLookupByLibrary.simpleMessage( - "Nossos aplicativos móveis são executados em segundo plano para criptografar e copiar com segurança quaisquer fotos novas que você acessar"), + "Nossos aplicativos móveis são executados em segundo plano para criptografar e fazer backup de quaisquer novas fotos que você clique"), "loadMessage8": MessageLookupByLibrary.simpleMessage( - "web.ente.io tem um enviador mais rápido"), + "web.ente.io tem um envio mais rápido"), "loadMessage9": MessageLookupByLibrary.simpleMessage( "Nós usamos Xchacha20Poly1305 para criptografar seus dados com segurança"), "loadingExifData": @@ -1232,95 +1177,95 @@ class MessageLookup extends MessageLookupByLibrary { "loadingGallery": MessageLookupByLibrary.simpleMessage("Carregando galeria..."), "loadingMessage": - MessageLookupByLibrary.simpleMessage("Carregando suas fotos..."), + MessageLookupByLibrary.simpleMessage("Carregar as suas fotos..."), "loadingModel": - MessageLookupByLibrary.simpleMessage("Baixando modelos..."), + MessageLookupByLibrary.simpleMessage("Transferindo modelos..."), "loadingYourPhotos": - MessageLookupByLibrary.simpleMessage("Carregando suas fotos..."), + MessageLookupByLibrary.simpleMessage("Carregar as suas fotos..."), "localGallery": MessageLookupByLibrary.simpleMessage("Galeria local"), "localIndexing": MessageLookupByLibrary.simpleMessage("Indexação local"), "localSyncErrorMessage": MessageLookupByLibrary.simpleMessage( - "Ocorreu um erro devido à sincronização de localização das fotos estar levando mais tempo que o esperado. Entre em contato conosco."), + "Parece que algo correu mal, uma vez que a sincronização de fotografias locais está a demorar mais tempo do que o esperado. Contacte a nossa equipa de apoio"), "location": MessageLookupByLibrary.simpleMessage("Localização"), "locationName": MessageLookupByLibrary.simpleMessage("Nome da localização"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Uma etiqueta de localização agrupa todas as fotos fotografadas em algum raio de uma foto"), + "Uma etiqueta de localização agrupa todas as fotos que foram tiradas num determinado raio de uma fotografia"), "locations": MessageLookupByLibrary.simpleMessage("Localizações"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"), - "lockscreen": MessageLookupByLibrary.simpleMessage("Tela de bloqueio"), - "logInLabel": MessageLookupByLibrary.simpleMessage("Entrar"), - "loggingOut": MessageLookupByLibrary.simpleMessage("Desconectando..."), + "lockscreen": MessageLookupByLibrary.simpleMessage("Ecrã de bloqueio"), + "logInLabel": MessageLookupByLibrary.simpleMessage("Iniciar sessão"), + "loggingOut": + MessageLookupByLibrary.simpleMessage("Terminar a sessão..."), "loginSessionExpired": MessageLookupByLibrary.simpleMessage("Sessão expirada"), "loginSessionExpiredDetails": MessageLookupByLibrary.simpleMessage( - "Sua sessão expirou. Registre-se novamente."), + "A sua sessão expirou. Por favor, inicie sessão novamente."), "loginTerms": MessageLookupByLibrary.simpleMessage( - "Ao clicar em entrar, eu concordo com os termos de serviço e a política de privacidade"), + "Ao clicar em iniciar sessão, eu concordo com os termos de serviço e política de privacidade"), "loginWithTOTP": - MessageLookupByLibrary.simpleMessage("Registrar com TOTP"), - "logout": MessageLookupByLibrary.simpleMessage("Encerrar sessão"), + MessageLookupByLibrary.simpleMessage("Iniciar sessão com TOTP"), + "logout": MessageLookupByLibrary.simpleMessage("Terminar sessão"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( - "Isso enviará através dos registros para ajudar-nos a resolver seu problema. Saiba que, nome de arquivos serão incluídos para ajudar a buscar problemas com arquivos específicos."), + "Isto enviará os registos para nos ajudar a resolver o problema. Tenha em atenção que os nomes dos ficheiros serão incluídos para ajudar a localizar problemas com ficheiros específicos."), "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( - "Pressione um e-mail para verificar a criptografia ponta a ponta."), + "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( - "Mantenha pressionado em um item para visualizá-lo em tela cheia"), + "Pressione e segure em um item para ver em tela cheia"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": - MessageLookupByLibrary.simpleMessage("Repetir vídeo desativado"), + MessageLookupByLibrary.simpleMessage("Repetir vídeo desligado"), "loopVideoOn": - MessageLookupByLibrary.simpleMessage("Repetir vídeo ativado"), + MessageLookupByLibrary.simpleMessage("Repetir vídeo ligado"), "lostDevice": - MessageLookupByLibrary.simpleMessage("Perdeu o dispositivo?"), + MessageLookupByLibrary.simpleMessage("Perdeu o seu dispositívo?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Aprendizagem automática"), - "magicSearch": MessageLookupByLibrary.simpleMessage("Busca mágica"), + "magicSearch": MessageLookupByLibrary.simpleMessage("Pesquisa mágica"), "magicSearchHint": MessageLookupByLibrary.simpleMessage( - "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. \'flor\', \'carro vermelho\', \'identidade\'"), - "manage": MessageLookupByLibrary.simpleMessage("Gerenciar"), - "manageDeviceStorage": MessageLookupByLibrary.simpleMessage( - "Gerenciar cache do dispositivo"), + "A pesquisa mágica permite pesquisar fotos por seu conteúdo, por exemplo, \'flor\', \'carro vermelho\', \'documentos de identidade\'"), + "manage": MessageLookupByLibrary.simpleMessage("Gerir"), "manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage( "Reveja e limpe o armazenamento de cache local."), - "manageFamily": - MessageLookupByLibrary.simpleMessage("Gerenciar família"), - "manageLink": MessageLookupByLibrary.simpleMessage("Gerenciar link"), - "manageParticipants": MessageLookupByLibrary.simpleMessage("Gerenciar"), + "manageFamily": MessageLookupByLibrary.simpleMessage("Gerir família"), + "manageLink": MessageLookupByLibrary.simpleMessage("Gerir link"), + "manageParticipants": MessageLookupByLibrary.simpleMessage("Gerir"), "manageSubscription": - MessageLookupByLibrary.simpleMessage("Gerenciar assinatura"), + MessageLookupByLibrary.simpleMessage("Gerir subscrição"), "manualPairDesc": MessageLookupByLibrary.simpleMessage( - "Parear com PIN funciona com qualquer tela que queira visualizar seu álbum."), + "Emparelhar com PIN funciona com qualquer ecrã onde pretenda ver o seu álbum."), "map": MessageLookupByLibrary.simpleMessage("Mapa"), "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Eu"), - "memoryCount": m48, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Juntar com o existente"), - "mergedPhotos": MessageLookupByLibrary.simpleMessage("Fotos mescladas"), + "mergedPhotos": + MessageLookupByLibrary.simpleMessage("Fotos combinadas"), "mlConsent": MessageLookupByLibrary.simpleMessage( "Ativar aprendizagem automática"), "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( "Eu entendo, e desejo ativar a aprendizagem automática"), "mlConsentDescription": MessageLookupByLibrary.simpleMessage( - "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta."), + "Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.\n\n\nIsto acontecerá no seu dispositivo e todas as informações biométricas geradas serão encriptadas de ponta a ponta."), "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( - "Clique aqui para mais detalhes sobre este recurso na política de privacidade"), + "Por favor, clique aqui para mais detalhes sobre este recurso na nossa política de privacidade"), "mlConsentTitle": MessageLookupByLibrary.simpleMessage( "Ativar aprendizagem automática?"), "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."), + "Tenha em atenção que a aprendizagem automática resultará numa maior utilização da largura de banda e da bateria até que todos os itens sejam indexados. Considere utilizar a aplicação de ambiente de trabalho para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."), "mobileWebDesktop": - MessageLookupByLibrary.simpleMessage("Celular, Web, Computador"), - "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderado"), + MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), + "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderada"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( - "Altere o termo de busca ou tente consultar"), + "Modifique a sua consulta ou tente pesquisar por"), "moments": MessageLookupByLibrary.simpleMessage("Momentos"), "month": MessageLookupByLibrary.simpleMessage("mês"), "monthly": MessageLookupByLibrary.simpleMessage("Mensal"), @@ -1329,42 +1274,41 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Mais recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mais relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sob as montanhas"), - "moveItem": m49, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover fotos selecionadas para uma data"), - "moveToAlbum": - MessageLookupByLibrary.simpleMessage("Mover para o álbum"), + "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum"), "moveToHiddenAlbum": - MessageLookupByLibrary.simpleMessage("Mover ao álbum oculto"), - "movedSuccessfullyTo": m50, + MessageLookupByLibrary.simpleMessage("Mover para álbum oculto"), + "movedSuccessfullyTo": m51, "movedToTrash": - MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), + MessageLookupByLibrary.simpleMessage("Mover para o lixo"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Movendo arquivos para o álbum..."), + "Mover arquivos para o álbum..."), "name": MessageLookupByLibrary.simpleMessage("Nome"), - "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Nomear álbum"), + "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Nomear o álbum"), "networkConnectionRefusedErr": MessageLookupByLibrary.simpleMessage( - "Não foi possível conectar ao Ente, tente novamente mais tarde. Se o erro persistir, entre em contato com o suporte."), + "Não foi possível conectar ao Ente, tente novamente após algum tempo. Se o erro persistir, entre em contato com o suporte."), "networkHostLookUpErr": MessageLookupByLibrary.simpleMessage( - "Não foi possível conectar-se ao Ente, verifique suas configurações de rede e entre em contato com o suporte se o erro persistir."), + "Não foi possível estabelecer ligação ao Ente. Verifique as definições de rede e contacte o serviço de apoio se o erro persistir."), "never": MessageLookupByLibrary.simpleMessage("Nunca"), "newAlbum": MessageLookupByLibrary.simpleMessage("Novo álbum"), "newLocation": MessageLookupByLibrary.simpleMessage("Nova localização"), "newPerson": MessageLookupByLibrary.simpleMessage("Nova pessoa"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Novo intervalo"), "newToEnte": MessageLookupByLibrary.simpleMessage("Novo no Ente"), - "newest": MessageLookupByLibrary.simpleMessage("Mais recente"), - "next": MessageLookupByLibrary.simpleMessage("Próximo"), + "newest": MessageLookupByLibrary.simpleMessage("Recentes"), + "next": MessageLookupByLibrary.simpleMessage("Seguinte"), "no": MessageLookupByLibrary.simpleMessage("Não"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( - "Nenhum álbum compartilhado por você ainda"), + "Ainda não há álbuns partilhados por si"), "noDeviceFound": MessageLookupByLibrary.simpleMessage( "Nenhum dispositivo encontrado"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Nenhum"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Você não tem arquivos neste dispositivo que possam ser excluídos"), + "Você não tem arquivos neste dispositivo que possam ser apagados"), "noDuplicates": - MessageLookupByLibrary.simpleMessage("✨ Sem duplicatas"), + MessageLookupByLibrary.simpleMessage("✨ Sem duplicados"), "noEnteAccountExclamation": MessageLookupByLibrary.simpleMessage("Nenhuma conta Ente!"), "noExifData": MessageLookupByLibrary.simpleMessage("Sem dados EXIF"), @@ -1375,42 +1319,45 @@ class MessageLookup extends MessageLookupByLibrary { "noImagesWithLocation": MessageLookupByLibrary.simpleMessage( "Nenhuma imagem com localização"), "noInternetConnection": - MessageLookupByLibrary.simpleMessage("Sem conexão à internet"), + MessageLookupByLibrary.simpleMessage("Sem ligação à internet"), "noPhotosAreBeingBackedUpRightNow": MessageLookupByLibrary.simpleMessage( - "No momento não há fotos sendo copiadas com segurança"), + "No momento não há backup de fotos sendo feito"), "noPhotosFoundHere": MessageLookupByLibrary.simpleMessage( "Nenhuma foto encontrada aqui"), "noQuickLinksSelected": MessageLookupByLibrary.simpleMessage( "Nenhum link rápido selecionado"), - "noRecoveryKey": - MessageLookupByLibrary.simpleMessage("Sem chave de recuperação?"), + "noRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Não tem chave de recuperação?"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( - "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, seus dados não podem ser descriptografados sem sua senha ou chave de recuperação"), + "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, os seus dados não podem ser descriptografados sem a sua palavra-passe ou a sua chave de recuperação"), "noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"), - "noResultsFound": - MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"), - "noSuggestionsForPerson": m51, + "noResultsFound": MessageLookupByLibrary.simpleMessage( + "Não foram encontrados resultados"), + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( - "Nenhum bloqueio do sistema encontrado"), - "notPersonLabel": m52, + "Nenhum bloqueio de sistema encontrado"), + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Não é esta pessoa?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( - "Nada compartilhado com você ainda"), + "Ainda nada partilhado consigo"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage("Nada para ver aqui! 👀"), "notifications": MessageLookupByLibrary.simpleMessage("Notificações"), - "ok": MessageLookupByLibrary.simpleMessage("OK"), + "ok": MessageLookupByLibrary.simpleMessage("Ok"), "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( - "No ente"), + "Em ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Na estrada de novo"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), - "oops": MessageLookupByLibrary.simpleMessage("Ops"), + "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( - "Opa! Não foi possível salvar as edições"), + "Oops, não foi possível guardar as edições"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Ops, algo deu errado"), "openAlbumInBrowser": @@ -1418,74 +1365,75 @@ class MessageLookup extends MessageLookupByLibrary { "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( "Use o aplicativo da web para adicionar fotos a este álbum"), "openFile": MessageLookupByLibrary.simpleMessage("Abrir arquivo"), - "openSettings": MessageLookupByLibrary.simpleMessage("Abrir opções"), - "openTheItem": - MessageLookupByLibrary.simpleMessage("• Abra a foto ou vídeo"), + "openSettings": + MessageLookupByLibrary.simpleMessage("Abrir Definições"), + "openTheItem": MessageLookupByLibrary.simpleMessage("• Abra o item"), "openstreetmapContributors": MessageLookupByLibrary.simpleMessage( "Contribuidores do OpenStreetMap"), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( - "Opcional, tão curto como quiser..."), - "orMergeWithExistingPerson": - MessageLookupByLibrary.simpleMessage("Ou mesclar com existente"), + "Opcional, o mais breve que quiser..."), + "orMergeWithExistingPerson": MessageLookupByLibrary.simpleMessage( + "Ou combinar com já existente"), "orPickAnExistingOne": - MessageLookupByLibrary.simpleMessage("Ou escolha um existente"), + MessageLookupByLibrary.simpleMessage("Ou escolha um já existente"), "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( "ou escolher dos seus contatos"), - "pair": MessageLookupByLibrary.simpleMessage("Parear"), - "pairWithPin": MessageLookupByLibrary.simpleMessage("Parear com PIN"), + "pair": MessageLookupByLibrary.simpleMessage("Emparelhar"), + "pairWithPin": + MessageLookupByLibrary.simpleMessage("Emparelhar com PIN"), "pairingComplete": - MessageLookupByLibrary.simpleMessage("Pareamento concluído"), + MessageLookupByLibrary.simpleMessage("Emparelhamento concluído"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, - "passKeyPendingVerification": - MessageLookupByLibrary.simpleMessage("Verificação pendente"), + "partyWithThem": m55, + "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( + "A verificação ainda está pendente"), "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage( - "Verificação de chave de acesso"), - "password": MessageLookupByLibrary.simpleMessage("Senha"), - "passwordChangedSuccessfully": - MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), + "Verificação da chave de acesso"), + "password": MessageLookupByLibrary.simpleMessage("Palavra-passe"), + "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( + "Palavra-passe alterada com sucesso"), "passwordLock": - MessageLookupByLibrary.simpleMessage("Bloqueio por senha"), - "passwordStrength": m55, + MessageLookupByLibrary.simpleMessage("Bloqueio da palavra-passe"), + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( - "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas."), + "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( - "Nós não armazenamos esta senha, se você esquecer, nós não poderemos descriptografar seus dados"), + "Não armazenamos esta palavra-passe, se você a esquecer, não podemos desencriptar os seus dados"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"), "paymentFailed": MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( - "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), "people": MessageLookupByLibrary.simpleMessage("Pessoas"), - "peopleUsingYourCode": - MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"), + "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( + "Pessoas que utilizam seu código"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( - "Todos os itens na lixeira serão excluídos permanentemente\n\nEsta ação não pode ser desfeita"), + "Todos os itens no lixo serão permanentemente eliminados\n\n\nEsta ação não pode ser anulada"), "permanentlyDelete": - MessageLookupByLibrary.simpleMessage("Excluir permanentemente"), + MessageLookupByLibrary.simpleMessage("Eliminar permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( - "Excluir permanentemente do dispositivo?"), - "personIsAge": m57, + "Apagar permanentemente do dispositivo?"), + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nome da pessoa"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Companhia de pelos"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descrições das fotos"), "photoGridSize": - MessageLookupByLibrary.simpleMessage("Tamanho da grade de fotos"), + MessageLookupByLibrary.simpleMessage("Tamanho da grelha de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( - "Suas fotos adicionadas serão removidas do álbum"), - "photosCount": m60, + "As fotos adicionadas por si serão removidas do álbum"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "As fotos mantêm a diferença de tempo relativo"), @@ -1497,43 +1445,45 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproduzir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproduzir transmissão"), "playstoreSubscription": - MessageLookupByLibrary.simpleMessage("Assinatura da PlayStore"), + MessageLookupByLibrary.simpleMessage("Subscrição da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": MessageLookupByLibrary.simpleMessage( - "Verifique sua conexão com a internet e tente novamente."), + "Por favor, verifique a sua ligação à Internet e tente novamente."), "pleaseContactSupportAndWeWillBeHappyToHelp": MessageLookupByLibrary.simpleMessage( - "Entre em contato com support@ente.io e nós ficaremos felizes em ajudar!"), + "Por favor, entre em contato com support@ente.io e nós ficaremos felizes em ajudar!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), - "pleaseLoginAgain": - MessageLookupByLibrary.simpleMessage("Registre-se novamente"), + "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( + "Por favor, inicie sessão novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": - MessageLookupByLibrary.simpleMessage("Tente novamente"), + MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": - MessageLookupByLibrary.simpleMessage("Verifique o código inserido"), - "pleaseWait": MessageLookupByLibrary.simpleMessage("Aguarde..."), - "pleaseWaitDeletingAlbum": - MessageLookupByLibrary.simpleMessage("Aguarde, excluindo álbum"), + MessageLookupByLibrary.simpleMessage( + "Por favor, verifique se o código que você inseriu"), + "pleaseWait": + MessageLookupByLibrary.simpleMessage("Por favor, aguarde ..."), + "pleaseWaitDeletingAlbum": MessageLookupByLibrary.simpleMessage( + "Por favor aguarde, apagar o álbum"), "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( - "Por favor, aguarde mais algum tempo antes de tentar novamente"), + "Por favor, aguarde algum tempo antes de tentar novamente"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Aguarde um pouco, isso talvez leve um tempo."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": - MessageLookupByLibrary.simpleMessage("Preparando registros..."), + MessageLookupByLibrary.simpleMessage("Preparando logs..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar mais"), "pressAndHoldToPlayVideo": MessageLookupByLibrary.simpleMessage( "Pressione e segure para reproduzir o vídeo"), @@ -1542,33 +1492,33 @@ class MessageLookup extends MessageLookupByLibrary { "previous": MessageLookupByLibrary.simpleMessage("Anterior"), "privacy": MessageLookupByLibrary.simpleMessage("Privacidade"), "privacyPolicyTitle": - MessageLookupByLibrary.simpleMessage("Política de Privacidade"), + MessageLookupByLibrary.simpleMessage("Política de privacidade"), "privateBackups": - MessageLookupByLibrary.simpleMessage("Cópias privadas"), + MessageLookupByLibrary.simpleMessage("Backups privados"), "privateSharing": - MessageLookupByLibrary.simpleMessage("Compartilhamento privado"), + MessageLookupByLibrary.simpleMessage("Partilha privada"), "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), - "processed": MessageLookupByLibrary.simpleMessage("Processado"), "processing": MessageLookupByLibrary.simpleMessage("Processando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processando vídeos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link público criado"), "publicLinkEnabled": - MessageLookupByLibrary.simpleMessage("Link público ativo"), + MessageLookupByLibrary.simpleMessage("Link público ativado"), "queued": MessageLookupByLibrary.simpleMessage("Na fila"), "quickLinks": MessageLookupByLibrary.simpleMessage("Links rápidos"), "radius": MessageLookupByLibrary.simpleMessage("Raio"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Abrir ticket"), - "rateTheApp": - MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), - "rateUs": MessageLookupByLibrary.simpleMessage("Avaliar"), - "rateUsOnStore": m66, + "rateTheApp": MessageLookupByLibrary.simpleMessage("Avaliar aplicação"), + "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reatribuir \"Eu\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reatribuindo..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1577,71 +1527,72 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar conta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("A recuperação iniciou"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chave de recuperação"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Chave de recuperação copiada para a área de transferência"), "recoveryKeyOnForgotPassword": MessageLookupByLibrary.simpleMessage( - "Caso você esqueça sua senha, a única maneira de recuperar seus dados é com esta chave."), + "Se esquecer sua palavra-passe, a única maneira de recuperar os seus dados é com esta chave."), "recoveryKeySaveDescription": MessageLookupByLibrary.simpleMessage( - "Não armazenamos esta chave, salve esta chave de 24 palavras em um lugar seguro."), + "Não armazenamos essa chave, por favor, guarde esta chave de 24 palavras num lugar seguro."), "recoveryKeySuccessBody": MessageLookupByLibrary.simpleMessage( - "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança."), + "Ótimo! A sua chave de recuperação é válida. Obrigado por verificar.\n\nLembre-se de manter cópia de segurança da sua chave de recuperação."), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( "Chave de recuperação verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( - "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente."), - "recoveryReady": m69, + "A sua chave de recuperação é a única forma de recuperar as suas fotografias se se esquecer da sua palavra-passe. Pode encontrar a sua chave de recuperação em Definições > Conta.\n\n\nIntroduza aqui a sua chave de recuperação para verificar se a guardou corretamente."), + "recoveryReady": m70, "recoverySuccessful": - MessageLookupByLibrary.simpleMessage("Recuperação com sucesso!"), + MessageLookupByLibrary.simpleMessage("Recuperação bem sucedida!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Um contato confiável está tentando acessar sua conta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( - "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar)."), + "O dispositivo atual não é suficientemente poderoso para verificar a palavra-passe, mas podemos regenerar novamente de uma maneira que funcione no seu dispositivo.\n\nPor favor, iniciar sessão utilizando código de recuperação e gerar novamente a sua palavra-passe (pode utilizar a mesma se quiser)."), "recreatePasswordTitle": - MessageLookupByLibrary.simpleMessage("Redefinir senha"), + MessageLookupByLibrary.simpleMessage("Recriar palavra-passe"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), - "reenterPassword": - MessageLookupByLibrary.simpleMessage("Reinserir senha"), - "reenterPin": MessageLookupByLibrary.simpleMessage("Reinserir PIN"), + "reenterPassword": MessageLookupByLibrary.simpleMessage( + "Insira novamente a palavra-passe"), + "reenterPin": + MessageLookupByLibrary.simpleMessage("Inserir PIN novamente"), "referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage( - "Recomende seus amigos e duplique seu plano"), + "Recomende amigos e duplique o seu plano"), "referralStep1": MessageLookupByLibrary.simpleMessage( "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( - "2. Eles então se inscrevem num plano pago"), - "referralStep3": m71, + "2. Eles se inscrevem em um plano pago"), + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( - "As referências estão atualmente pausadas"), + "As referências estão atualmente em pausa"), "rejectRecovery": MessageLookupByLibrary.simpleMessage("Rejeitar recuperação"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( - "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado"), + "Esvazie também a opção “Eliminados recentemente” em “Definições” -> “Armazenamento” para reclamar o espaço libertado"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( - "Também esvazie sua \"Lixeira\" para reivindicar o espaço liberado"), + "Esvazie também o seu “Lixo” para reivindicar o espaço libertado"), "remoteImages": MessageLookupByLibrary.simpleMessage("Imagens remotas"), "remoteThumbnails": MessageLookupByLibrary.simpleMessage("Miniaturas remotas"), "remoteVideos": MessageLookupByLibrary.simpleMessage("Vídeos remotos"), "remove": MessageLookupByLibrary.simpleMessage("Remover"), "removeDuplicates": - MessageLookupByLibrary.simpleMessage("Excluir duplicatas"), + MessageLookupByLibrary.simpleMessage("Remover duplicados"), "removeDuplicatesDesc": MessageLookupByLibrary.simpleMessage( - "Revise e remova arquivos que são duplicatas exatas."), + "Rever e remover ficheiros que sejam duplicados exatos."), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("Remover do álbum"), "removeFromAlbumTitle": - MessageLookupByLibrary.simpleMessage("Remover do álbum?"), + MessageLookupByLibrary.simpleMessage("Remover do álbum"), "removeFromFavorite": - MessageLookupByLibrary.simpleMessage("Desfavoritar"), + MessageLookupByLibrary.simpleMessage("Remover dos favoritos"), "removeInvite": MessageLookupByLibrary.simpleMessage("Remover convite"), "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1660,15 +1611,15 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("Renomear álbum"), "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": - MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m73, - "reportABug": MessageLookupByLibrary.simpleMessage("Informar um erro"), - "reportBug": MessageLookupByLibrary.simpleMessage("Informar erro"), + MessageLookupByLibrary.simpleMessage("Renovar subscrição"), + "renewsOn": m74, + "reportABug": MessageLookupByLibrary.simpleMessage("Reporte um bug"), + "reportBug": MessageLookupByLibrary.simpleMessage("Reportar bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), - "resetIgnoredFiles": MessageLookupByLibrary.simpleMessage( - "Redefinir arquivos ignorados"), + "resetIgnoredFiles": + MessageLookupByLibrary.simpleMessage("Repor ficheiros ignorados"), "resetPasswordTitle": - MessageLookupByLibrary.simpleMessage("Redefinir senha"), + MessageLookupByLibrary.simpleMessage("Redefinir palavra-passe"), "resetPerson": MessageLookupByLibrary.simpleMessage("Remover"), "resetToDefault": MessageLookupByLibrary.simpleMessage("Redefinir para o padrão"), @@ -1676,83 +1627,79 @@ class MessageLookup extends MessageLookupByLibrary { "restoreToAlbum": MessageLookupByLibrary.simpleMessage("Restaurar para álbum"), "restoringFiles": - MessageLookupByLibrary.simpleMessage("Restaurando arquivos..."), + MessageLookupByLibrary.simpleMessage("Restaurar arquivos..."), "resumableUploads": - MessageLookupByLibrary.simpleMessage("Envios retomáveis"), + MessageLookupByLibrary.simpleMessage("Uploads reenviados"), "retry": MessageLookupByLibrary.simpleMessage("Tentar novamente"), - "review": MessageLookupByLibrary.simpleMessage("Revisar"), + "review": MessageLookupByLibrary.simpleMessage("Rever"), "reviewDeduplicateItems": MessageLookupByLibrary.simpleMessage( - "Reveja e exclua os itens que você acredita serem duplicados."), + "Reveja e elimine os itens que considera serem duplicados."), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugestões"), "right": MessageLookupByLibrary.simpleMessage("Direita"), - "roadtripWithThem": m74, - "rotate": MessageLookupByLibrary.simpleMessage("Girar"), + "roadtripWithThem": m75, + "rotate": MessageLookupByLibrary.simpleMessage("Rodar"), "rotateLeft": - MessageLookupByLibrary.simpleMessage("Girar para a esquerda"), + MessageLookupByLibrary.simpleMessage("Rodar para a esquerda"), "rotateRight": - MessageLookupByLibrary.simpleMessage("Girar para a direita"), + MessageLookupByLibrary.simpleMessage("Rodar para a direita"), "safelyStored": MessageLookupByLibrary.simpleMessage("Armazenado com segurança"), - "save": MessageLookupByLibrary.simpleMessage("Salvar"), + "save": MessageLookupByLibrary.simpleMessage("Guardar"), "saveChangesBeforeLeavingQuestion": MessageLookupByLibrary.simpleMessage( "Salvar mudanças antes de sair?"), - "saveCollage": MessageLookupByLibrary.simpleMessage("Salvar colagem"), - "saveCopy": MessageLookupByLibrary.simpleMessage("Salvar cópia"), - "saveKey": MessageLookupByLibrary.simpleMessage("Salvar chave"), - "savePerson": MessageLookupByLibrary.simpleMessage("Salvar pessoa"), + "saveCollage": MessageLookupByLibrary.simpleMessage("Guardar colagem"), + "saveCopy": MessageLookupByLibrary.simpleMessage("Guardar cópia"), + "saveKey": MessageLookupByLibrary.simpleMessage("Guardar chave"), + "savePerson": MessageLookupByLibrary.simpleMessage("Guardar pessoa"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( - "Salve sua chave de recuperação, se você ainda não fez"), - "saving": MessageLookupByLibrary.simpleMessage("Salvando..."), + "Guarde a sua chave de recuperação, caso ainda não o tenha feito"), + "saving": MessageLookupByLibrary.simpleMessage("A gravar..."), "savingEdits": - MessageLookupByLibrary.simpleMessage("Salvando edições..."), - "scanCode": MessageLookupByLibrary.simpleMessage("Escanear código"), + MessageLookupByLibrary.simpleMessage("Gravando edições..."), + "scanCode": MessageLookupByLibrary.simpleMessage("Ler código Qr"), "scanThisBarcodeWithnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( - "Escaneie este código de barras com\no aplicativo autenticador"), - "search": MessageLookupByLibrary.simpleMessage("Buscar"), + "Leia este código com a sua aplicação dois fatores."), + "search": MessageLookupByLibrary.simpleMessage("Pesquisar"), "searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("Álbuns"), "searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("Nome do álbum"), "searchByExamples": MessageLookupByLibrary.simpleMessage( - "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (ex.: \"2022\", \"Janeiro\")\n• Temporadas (ex.: \"Natal\")\n• Tags (ex.: \"#divertido\")"), + "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (e.. \"2022\", \"Janeiro\")\n• Feriados (por exemplo, \"Natal\")\n• Descrições de fotos (por exemplo, \"#divertido\")"), "searchCaptionEmptySection": MessageLookupByLibrary.simpleMessage( - "Adicione marcações como \"#viagem\" nas informações das fotos para encontrá-las aqui com facilidade"), - "searchDatesEmptySection": - MessageLookupByLibrary.simpleMessage("Buscar por data, mês ou ano"), - "searchDiscoverEmptySection": MessageLookupByLibrary.simpleMessage( - "As imagens serão exibidas aqui quando o processamento e sincronização for concluído"), + "Adicione descrições como \"#trip\" nas informações das fotos para encontrá-las aqui rapidamente"), + "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( + "Pesquisar por data, mês ou ano"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( - "As pessoas apareceram aqui quando a indexação for concluída"), + "As pessoas serão mostradas aqui quando a indexação estiver concluída"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "searchHint1": - MessageLookupByLibrary.simpleMessage("busca rápida no dispositivo"), + "searchHint1": MessageLookupByLibrary.simpleMessage( + "Pesquisa rápida no dispositivo"), "searchHint2": - MessageLookupByLibrary.simpleMessage("Descrições e data das fotos"), + MessageLookupByLibrary.simpleMessage("Datas das fotos, descrições"), "searchHint3": MessageLookupByLibrary.simpleMessage( "Álbuns, nomes de arquivos e tipos"), - "searchHint4": MessageLookupByLibrary.simpleMessage("Localização"), + "searchHint4": MessageLookupByLibrary.simpleMessage("Local"), "searchHint5": MessageLookupByLibrary.simpleMessage( - "Em breve: Busca mágica e rostos ✨"), + "Em breve: Rostos e pesquisa mágica ✨"), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( - "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui"), - "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( - "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "Convide pessoas e verá todas as fotos partilhadas por elas aqui"), + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver links de álbum compartilhado no aplicativo"), "selectALocation": - MessageLookupByLibrary.simpleMessage("Selecionar localização"), + MessageLookupByLibrary.simpleMessage("Selecione uma localização"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( - "Primeiramente selecione uma localização"), + "Selecione uma localização primeiro"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Selecionar álbum"), "selectAll": MessageLookupByLibrary.simpleMessage("Selecionar tudo"), "selectAllShort": MessageLookupByLibrary.simpleMessage("Tudo"), @@ -1760,11 +1707,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selecionar foto da capa"), "selectDate": MessageLookupByLibrary.simpleMessage("Selecionar data"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( - "Selecionar pastas para copiar com segurança"), + "Selecionar pastas para cópia de segurança"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage( "Selecionar itens para adicionar"), "selectLanguage": - MessageLookupByLibrary.simpleMessage("Selecionar idioma"), + MessageLookupByLibrary.simpleMessage("Selecionar Idioma"), "selectMailApp": MessageLookupByLibrary.simpleMessage( "Selecionar aplicativo de e-mail"), "selectMorePhotos": @@ -1775,119 +1722,121 @@ class MessageLookup extends MessageLookupByLibrary { "Selecione uma data e hora para todos"), "selectPersonToLink": MessageLookupByLibrary.simpleMessage( "Selecione a pessoa para vincular"), - "selectReason": MessageLookupByLibrary.simpleMessage("Diga o motivo"), + "selectReason": + MessageLookupByLibrary.simpleMessage("Selecionar motivo"), "selectStartOfRange": MessageLookupByLibrary.simpleMessage( "Selecionar início de intervalo"), "selectTime": MessageLookupByLibrary.simpleMessage("Selecionar tempo"), "selectYourFace": MessageLookupByLibrary.simpleMessage("Selecione seu rosto"), "selectYourPlan": - MessageLookupByLibrary.simpleMessage("Selecione seu plano"), + MessageLookupByLibrary.simpleMessage("Selecione o seu plano"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Os arquivos selecionados não estão no Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( - "As pastas selecionadas serão criptografadas e armazenadas em copiadas com segurança"), + "As pastas selecionadas serão encriptadas e guardadas como cópia de segurança"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( - "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira."), + "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo."), "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos desta pessoa, entretanto não serão excluídos da sua biblioteca."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), - "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), + "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), "sendLink": MessageLookupByLibrary.simpleMessage("Enviar link"), "serverEndpoint": - MessageLookupByLibrary.simpleMessage("Ponto final do servidor"), + MessageLookupByLibrary.simpleMessage("Endpoint do servidor"), "sessionExpired": MessageLookupByLibrary.simpleMessage("Sessão expirada"), "sessionIdMismatch": MessageLookupByLibrary.simpleMessage( "Incompatibilidade de ID de sessão"), - "setAPassword": MessageLookupByLibrary.simpleMessage("Definir senha"), + "setAPassword": + MessageLookupByLibrary.simpleMessage("Definir uma palavra-passe"), "setAs": MessageLookupByLibrary.simpleMessage("Definir como"), "setCover": MessageLookupByLibrary.simpleMessage("Definir capa"), "setLabel": MessageLookupByLibrary.simpleMessage("Definir"), "setNewPassword": - MessageLookupByLibrary.simpleMessage("Definir nova senha"), - "setNewPin": MessageLookupByLibrary.simpleMessage("Definir PIN novo"), + MessageLookupByLibrary.simpleMessage("Definir nova palavra-passe"), + "setNewPin": MessageLookupByLibrary.simpleMessage("Definir novo PIN"), "setPasswordTitle": - MessageLookupByLibrary.simpleMessage("Definir senha"), + MessageLookupByLibrary.simpleMessage("Definir palavra-passe"), "setRadius": MessageLookupByLibrary.simpleMessage("Definir raio"), "setupComplete": MessageLookupByLibrary.simpleMessage("Configuração concluída"), - "share": MessageLookupByLibrary.simpleMessage("Compartilhar"), - "shareALink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), + "share": MessageLookupByLibrary.simpleMessage("Partilhar"), + "shareALink": MessageLookupByLibrary.simpleMessage("Partilhar um link"), "shareAlbumHint": MessageLookupByLibrary.simpleMessage( - "Abra um álbum e toque no botão compartilhar no canto superior direito para compartilhar."), + "Abra um álbum e toque no botão de partilha no canto superior direito para partilhar"), "shareAnAlbumNow": - MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), - "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m79, + MessageLookupByLibrary.simpleMessage("Partilhar um álbum"), + "shareLink": MessageLookupByLibrary.simpleMessage("Partilhar link"), + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( - "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m80, + "Partilhar apenas com as pessoas que deseja"), + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( - "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io"), + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( - "Compartilhar com usuários não ente"), - "shareWithPeopleSectionTitle": m82, + "Compartilhar com usuários que não usam Ente"), + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( - "Compartilhar seu primeiro álbum"), + "Partilhe o seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( - "Criar álbuns compartilhados e colaborativos com outros usuários Ente, incluindo usuários em planos gratuitos."), + "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos."), "sharedByMe": - MessageLookupByLibrary.simpleMessage("Compartilhada por mim"), + MessageLookupByLibrary.simpleMessage("Partilhado por mim"), "sharedByYou": - MessageLookupByLibrary.simpleMessage("Compartilhado por você"), + MessageLookupByLibrary.simpleMessage("Partilhado por si"), "sharedPhotoNotifications": - MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), + MessageLookupByLibrary.simpleMessage("Novas fotos partilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( - "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte"), - "sharedWith": m83, + "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte"), + "sharedWith": m85, "sharedWithMe": - MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), + MessageLookupByLibrary.simpleMessage("Partilhado comigo"), "sharedWithYou": - MessageLookupByLibrary.simpleMessage("Compartilhado com você"), - "sharing": MessageLookupByLibrary.simpleMessage("Compartilhando..."), + MessageLookupByLibrary.simpleMessage("Partilhado consigo"), + "sharing": MessageLookupByLibrary.simpleMessage("Partilhar..."), "shiftDatesAndTime": MessageLookupByLibrary.simpleMessage("Alterar as datas e horas"), "showMemories": MessageLookupByLibrary.simpleMessage("Mostrar memórias"), "showPerson": MessageLookupByLibrary.simpleMessage("Mostrar pessoa"), "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage( - "Sair da conta em outros dispositivos"), + "Terminar sessão noutros dispositivos"), "signOutOtherBody": MessageLookupByLibrary.simpleMessage( - "Se você acha que alguém possa saber da sua senha, você pode forçar desconectar sua conta de outros dispositivos."), - "signOutOtherDevices": - MessageLookupByLibrary.simpleMessage("Sair em outros dispositivos"), + "Se pensa que alguém pode saber a sua palavra-passe, pode forçar todos os outros dispositivos que utilizam a sua conta a terminar a sessão."), + "signOutOtherDevices": MessageLookupByLibrary.simpleMessage( + "Terminar a sessão noutros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( - "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m84, + "Eu concordo com os termos de serviço e política de privacidade"), + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( - "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "Será eliminado de todos os álbuns."), + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), - "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), + "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( - "Alguns itens estão em ambos o Ente quanto no seu dispositivo."), + "Alguns itens estão tanto no Ente como no seu dispositivo."), "someOfTheFilesYouAreTryingToDeleteAre": MessageLookupByLibrary.simpleMessage( - "Alguns dos arquivos que você está tentando excluir só estão disponíveis no seu dispositivo e não podem ser recuperados se forem excluídos"), + "Alguns dos ficheiros que está a tentar eliminar só estão disponíveis no seu dispositivo e não podem ser recuperados se forem eliminados"), "someoneSharingAlbumsWithYouShouldSeeTheSameId": MessageLookupByLibrary.simpleMessage( - "Alguém compartilhando álbuns com você deve ver o mesmo ID no dispositivo."), + "Alguém compartilhando álbuns com você deve ver o mesmo ID no seu dispositivo."), "somethingWentWrong": - MessageLookupByLibrary.simpleMessage("Algo deu errado"), + MessageLookupByLibrary.simpleMessage("Ocorreu um erro"), "somethingWentWrongPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Algo deu errado. Tente outra vez"), + "Ocorreu um erro. Tente novamente"), "sorry": MessageLookupByLibrary.simpleMessage("Desculpe"), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Desculpe, não foi possível adicionar aos favoritos!"), @@ -1896,46 +1845,48 @@ class MessageLookup extends MessageLookupByLibrary { "Desculpe, não foi possível remover dos favoritos!"), "sorryTheCodeYouveEnteredIsIncorrect": MessageLookupByLibrary.simpleMessage( - "O código inserido está incorreto"), + "Desculpe, o código inserido está incorreto"), "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( - "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\ninicie sessão com um dispositivo diferente."), + "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\npor favor iniciar sessão com um dispositivo diferente."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordenar"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordenar por"), "sortNewestFirst": - MessageLookupByLibrary.simpleMessage("Recentes primeiro"), + MessageLookupByLibrary.simpleMessage("Mais recentes primeiro"), "sortOldestFirst": - MessageLookupByLibrary.simpleMessage("Antigos primeiro"), + MessageLookupByLibrary.simpleMessage("Mais antigos primeiro"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Sucesso"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Destacar si mesmo"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("Iniciar recuperação"), "startBackup": MessageLookupByLibrary.simpleMessage("Iniciar cópia de segurança"), - "status": MessageLookupByLibrary.simpleMessage("Estado"), - "stopCastingBody": - MessageLookupByLibrary.simpleMessage("Deseja parar a transmissão?"), + "status": MessageLookupByLibrary.simpleMessage("Status"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Queres parar de fazer transmissão?"), "stopCastingTitle": MessageLookupByLibrary.simpleMessage("Parar transmissão"), "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), - "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m89, + "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalhes da transmissão"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, - "subscribe": MessageLookupByLibrary.simpleMessage("Inscrever-se"), + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, + "subscribe": MessageLookupByLibrary.simpleMessage("Subscrever"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( - "Você precisa de uma inscrição paga ativa para ativar o compartilhamento."), - "subscription": MessageLookupByLibrary.simpleMessage("Assinatura"), + "Você precisa de uma assinatura paga ativa para ativar o compartilhamento."), + "subscription": MessageLookupByLibrary.simpleMessage("Subscrição"), "success": MessageLookupByLibrary.simpleMessage("Sucesso"), "successfullyArchived": MessageLookupByLibrary.simpleMessage("Arquivado com sucesso"), @@ -1944,12 +1895,12 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnarchived": MessageLookupByLibrary.simpleMessage("Desarquivado com sucesso"), "successfullyUnhid": - MessageLookupByLibrary.simpleMessage("Desocultado com sucesso"), + MessageLookupByLibrary.simpleMessage("Reexibido com sucesso"), "suggestFeatures": - MessageLookupByLibrary.simpleMessage("Sugerir recurso"), + MessageLookupByLibrary.simpleMessage("Sugerir recursos"), "sunrise": MessageLookupByLibrary.simpleMessage("No horizonte"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1961,18 +1912,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Toque para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toque para enviar"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( - "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "terminate": MessageLookupByLibrary.simpleMessage("Encerrar"), - "terminateSession": MessageLookupByLibrary.simpleMessage("Sair?"), + "Parece que algo correu mal. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contacto com a nossa equipa de suporte."), + "terminate": MessageLookupByLibrary.simpleMessage("Terminar"), + "terminateSession": + MessageLookupByLibrary.simpleMessage("Terminar sessão?"), "terms": MessageLookupByLibrary.simpleMessage("Termos"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Termos"), "thankYou": MessageLookupByLibrary.simpleMessage("Obrigado"), - "thankYouForSubscribing": - MessageLookupByLibrary.simpleMessage("Obrigado por assinar!"), + "thankYouForSubscribing": MessageLookupByLibrary.simpleMessage( + "Obrigado pela sua subscrição!"), "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage( - "A instalação não pôde ser concluída"), + "Não foi possível concluir o download."), "theLinkYouAreTryingToAccessHasExpired": MessageLookupByLibrary.simpleMessage( "O link que você está tentando acessar já expirou."), @@ -1982,10 +1934,10 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("Tema"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( - "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m95, + "Estes itens serão eliminados do seu dispositivo."), + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( - "Eles serão excluídos de todos os álbuns."), + "Serão eliminados de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( "Esta ação não pode ser desfeita"), "thisAlbumAlreadyHDACollaborativeLink": @@ -1993,56 +1945,56 @@ class MessageLookup extends MessageLookupByLibrary { "Este álbum já tem um link colaborativo"), "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( - "Isso pode ser usado para recuperar sua conta se você perder seu segundo fator"), + "Isto pode ser usado para recuperar sua conta se você perder seu segundo fator"), "thisDevice": MessageLookupByLibrary.simpleMessage("Este dispositivo"), - "thisEmailIsAlreadyInUse": MessageLookupByLibrary.simpleMessage( - "Este e-mail já está sendo usado"), + "thisEmailIsAlreadyInUse": + MessageLookupByLibrary.simpleMessage("Este email já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( - "Esta imagem não possui dados EXIF"), + "Esta imagem não tem dados exif"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Este é você!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana com o passar dos anos"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( - "Isso fará você sair do dispositivo a seguir:"), + "Irá desconectar a sua conta do seguinte dispositivo:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( - "Isso fará você sair deste dispositivo!"), + "Irá desconectar a sua conta do seu dispositivo!"), "thisWillMakeTheDateAndTimeOfAllSelected": MessageLookupByLibrary.simpleMessage( "Isso fará que a data e hora de todas as fotos selecionadas fiquem iguais."), "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Isto removerá links públicos de todos os links rápidos selecionados."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( - "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), + "Para ativar o bloqueio de aplicações, configure o código de acesso do dispositivo ou o bloqueio de ecrã nas definições do sistema."), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( - "Para ocultar uma foto ou vídeo"), + "Para ocultar uma foto ou um vídeo"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( - "Para redefinir sua senha, verifique seu e-mail primeiramente."), - "todaysLogs": MessageLookupByLibrary.simpleMessage("Registros de hoje"), + "Para redefinir a sua palavra-passe, verifique primeiro o seu e-mail."), + "todaysLogs": MessageLookupByLibrary.simpleMessage("Logs de hoje"), "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage( "Muitas tentativas incorretas"), "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), - "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m99, - "trim": MessageLookupByLibrary.simpleMessage("Recortar"), - "tripInYear": m100, - "tripToLocation": m101, + "trash": MessageLookupByLibrary.simpleMessage("Lixo"), + "trashDaysLeft": m101, + "trim": MessageLookupByLibrary.simpleMessage("Cortar"), + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatos confiáveis"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( - "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente."), - "twitter": MessageLookupByLibrary.simpleMessage("Twitter/X"), + "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o Ente."), + "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 meses grátis em planos anuais"), "twofactor": MessageLookupByLibrary.simpleMessage("Dois fatores"), @@ -2054,23 +2006,23 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores"), "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage( - "Autenticação de dois fatores redefinida com sucesso"), + "Autenticação de dois fatores redefinida com êxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), - "unarchiving": MessageLookupByLibrary.simpleMessage("Desarquivando..."), + "unarchiving": MessageLookupByLibrary.simpleMessage("Desarquivar..."), "unavailableReferralCode": MessageLookupByLibrary.simpleMessage( - "Desculpe, este código está indisponível."), + "Desculpe, este código não está disponível."), "uncategorized": MessageLookupByLibrary.simpleMessage("Sem categoria"), - "unhide": MessageLookupByLibrary.simpleMessage("Desocultar"), + "unhide": MessageLookupByLibrary.simpleMessage("Mostrar"), "unhideToAlbum": - MessageLookupByLibrary.simpleMessage("Desocultar para o álbum"), + MessageLookupByLibrary.simpleMessage("Mostrar para o álbum"), "unhiding": MessageLookupByLibrary.simpleMessage("Reexibindo..."), "unhidingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Desocultando arquivos para o álbum"), + "Desocultar ficheiros para o álbum"), "unlock": MessageLookupByLibrary.simpleMessage("Desbloquear"), "unpinAlbum": MessageLookupByLibrary.simpleMessage("Desafixar álbum"), "unselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar tudo"), @@ -2080,14 +2032,14 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( - "Enviando arquivos para o álbum..."), - "uploadingMultipleMemories": m105, + "Enviar ficheiros para o álbum..."), + "uploadingMultipleMemories": m107, "uploadingSingleMemory": - MessageLookupByLibrary.simpleMessage("Preservando 1 memória..."), + MessageLookupByLibrary.simpleMessage("Preservar 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( - "Com 50% de desconto, até 4 de dezembro"), + "Até 50% de desconto, até 4 de dezembro."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano."), "useAsCover": MessageLookupByLibrary.simpleMessage("Usar como capa"), @@ -2099,51 +2051,49 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Usar chave de recuperação"), "useSelectedPhoto": - MessageLookupByLibrary.simpleMessage("Usar foto selecionada"), - "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço usado"), - "validTill": m106, + MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), + "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço utilizado"), + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( - "Falha na verificação. Tente novamente"), + "Falha na verificação, por favor tente novamente"), "verificationId": - MessageLookupByLibrary.simpleMessage("ID de verificação"), + MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), - "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), - "verifyEmailID": m107, + "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), "verifyPassword": - MessageLookupByLibrary.simpleMessage("Verificar senha"), - "verifying": MessageLookupByLibrary.simpleMessage("Verificando..."), + MessageLookupByLibrary.simpleMessage("Verificar palavra-passe"), + "verifying": MessageLookupByLibrary.simpleMessage("A verificar…"), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( "Verificando chave de recuperação..."), "videoInfo": - MessageLookupByLibrary.simpleMessage("Informações do vídeo"), + MessageLookupByLibrary.simpleMessage("Informação de Vídeo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), "videoStreaming": MessageLookupByLibrary.simpleMessage("Transmissão de vídeo"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sessões ativas"), - "viewAddOnButton": - MessageLookupByLibrary.simpleMessage("Ver complementos"), + "viewAddOnButton": MessageLookupByLibrary.simpleMessage("Ver addons"), "viewAll": MessageLookupByLibrary.simpleMessage("Ver tudo"), "viewAllExifData": MessageLookupByLibrary.simpleMessage("Ver todos os dados EXIF"), "viewLargeFiles": - MessageLookupByLibrary.simpleMessage("Arquivos grandes"), + MessageLookupByLibrary.simpleMessage("Ficheiros grandes"), "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( - "Ver arquivos que consumem a maior parte do armazenamento."), - "viewLogs": MessageLookupByLibrary.simpleMessage("Ver registros"), + "Ver os ficheiros que estão a consumir a maior quantidade de armazenamento."), + "viewLogs": MessageLookupByLibrary.simpleMessage("Ver logs"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver chave de recuperação"), "viewer": MessageLookupByLibrary.simpleMessage("Visualizador"), - "viewersSuccessfullyAdded": m108, "visitWebToManage": MessageLookupByLibrary.simpleMessage( - "Visite o web.ente.io para gerenciar sua assinatura"), + "Visite web.ente.io para gerir a sua subscrição"), "waitingForVerification": - MessageLookupByLibrary.simpleMessage("Esperando verificação..."), + MessageLookupByLibrary.simpleMessage("Aguardando verificação..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage("Aguardando Wi-Fi..."), "warning": MessageLookupByLibrary.simpleMessage("Aviso"), @@ -2151,8 +2101,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nós somos de código aberto!"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( - "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m109, + "Não suportamos a edição de fotos e álbuns que ainda não possui"), + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), @@ -2161,68 +2111,65 @@ class MessageLookup extends MessageLookupByLibrary { "Um contato confiável pode ajudá-lo em recuperar seus dados."), "yearShort": MessageLookupByLibrary.simpleMessage("ano"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), - "yesCancel": MessageLookupByLibrary.simpleMessage("Sim"), + "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( "Sim, converter para visualizador"), - "yesDelete": MessageLookupByLibrary.simpleMessage("Sim, excluir"), + "yesDelete": MessageLookupByLibrary.simpleMessage("Sim, apagar"), "yesDiscardChanges": - MessageLookupByLibrary.simpleMessage("Sim, descartar alterações"), + MessageLookupByLibrary.simpleMessage("Sim, rejeitar alterações"), "yesLogout": - MessageLookupByLibrary.simpleMessage("Sim, encerrar sessão"), - "yesRemove": MessageLookupByLibrary.simpleMessage("Sim, excluir"), - "yesRenew": MessageLookupByLibrary.simpleMessage("Sim"), + MessageLookupByLibrary.simpleMessage("Sim, terminar sessão"), + "yesRemove": MessageLookupByLibrary.simpleMessage("Sim, remover"), + "yesRenew": MessageLookupByLibrary.simpleMessage("Sim, Renovar"), "yesResetPerson": - MessageLookupByLibrary.simpleMessage("Sim, redefinir pessoa"), - "you": MessageLookupByLibrary.simpleMessage("Você"), - "youAndThem": m111, + MessageLookupByLibrary.simpleMessage("Sim, repor pessoa"), + "you": MessageLookupByLibrary.simpleMessage("Tu"), + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Você está em um plano familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( - "Você está na versão mais recente"), + "Está a utilizar a versão mais recente"), "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( - "* Você pode duplicar seu armazenamento ao máximo"), + "* Você pode duplicar seu armazenamento no máximo"), "youCanManageYourLinksInTheShareTab": MessageLookupByLibrary.simpleMessage( - "Você pode gerenciar seus links na aba de compartilhamento."), + "Pode gerir as suas ligações no separador partilhar."), "youCanTrySearchingForADifferentQuery": MessageLookupByLibrary.simpleMessage( - "Você pode tentar buscar por outra consulta."), + "Pode tentar pesquisar uma consulta diferente."), "youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage( - "Você não pode rebaixar para este plano"), + "Não é possível fazer o downgrade para este plano"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( - "Você não pode compartilhar consigo mesmo"), + "Não podes partilhar contigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( - "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m112, + "Não tem nenhum item arquivado."), + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": - MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), + MessageLookupByLibrary.simpleMessage("A sua conta foi eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), "yourPlanWasSuccessfullyDowngraded": MessageLookupByLibrary.simpleMessage( - "Seu plano foi rebaixado com sucesso"), + "O seu plano foi rebaixado com sucesso"), "yourPlanWasSuccessfullyUpgraded": MessageLookupByLibrary.simpleMessage( - "Seu plano foi atualizado com sucesso"), + "O seu plano foi atualizado com sucesso"), "yourPurchaseWasSuccessful": MessageLookupByLibrary.simpleMessage( - "Sua compra foi efetuada com sucesso"), + "Sua compra foi realizada com sucesso"), "yourStorageDetailsCouldNotBeFetched": MessageLookupByLibrary.simpleMessage( - "Seus detalhes de armazenamento não puderam ser obtidos"), + "Não foi possível obter os seus dados de armazenamento"), "yourSubscriptionHasExpired": - MessageLookupByLibrary.simpleMessage("A sua assinatura expirou"), + MessageLookupByLibrary.simpleMessage("A sua subscrição expirou"), "yourSubscriptionWasUpdatedSuccessfully": MessageLookupByLibrary.simpleMessage( - "Sua assinatura foi atualizada com sucesso"), + "A sua subscrição foi actualizada com sucesso"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( - "O código de verificação expirou"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Você não possui nenhum arquivo duplicado que possa ser excluído"), + "O seu código de verificação expirou"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Você não tem arquivos neste álbum que possam ser excluídos"), + "Não existem ficheiros neste álbum que possam ser eliminados"), "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage( - "Reduzir ampliação para ver as fotos") + "Diminuir o zoom para ver fotos") }; } diff --git a/mobile/lib/generated/intl/messages_pt_BR.dart b/mobile/lib/generated/intl/messages_pt_BR.dart index 1c3f5c89c2..fff8e542c0 100644 --- a/mobile/lib/generated/intl/messages_pt_BR.dart +++ b/mobile/lib/generated/intl/messages_pt_BR.dart @@ -97,223 +97,227 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "E-mail alterado para ${newEmail}"; + static String m27(name) => "Este e-mail já está vinculado a ${name}."; - static String m28(email) => "${email} não possui uma conta Ente."; + static String m28(newEmail) => "E-mail alterado para ${newEmail}"; - static String m29(email) => + static String m29(email) => "${email} não possui uma conta Ente."; + + static String m30(email) => "${email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos."; - static String m30(name) => "Abraçando ${name}"; + static String m31(name) => "Abraçando ${name}"; - static String m31(text) => "Fotos adicionais encontradas para ${text}"; + static String m32(text) => "Fotos adicionais encontradas para ${text}"; - static String m32(name) => "Tendo banquete com ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + static String m33(name) => "Tendo banquete com ${name}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste álbum foi copiado com segurança"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código"; - static String m36(endDate) => "A avaliação grátis acaba em ${endDate}"; + static String m37(endDate) => "A avaliação grátis acaba em ${endDate}"; - static String m37(count) => + static String m38(count) => "Você ainda pode acessá${Intl.plural(count, one: '-lo', other: '-los')} no Ente se você tiver uma assinatura ativa"; - static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Ele pode excluído para liberar ${formattedSize}', other: 'Eles podem ser excluídos do dispositivo para liberar ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Caminhando com ${name}"; + static String m42(name) => "Caminhando com ${name}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m43(name) => "Últimos momentos com ${name}"; + static String m44(name) => "Últimos momentos com ${name}"; - static String m44(email) => + static String m45(email) => "${email} convidou você para ser um contato confiável"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m46(email) => "Vincular pessoa a ${email}"; + static String m47(email) => "Vincular pessoa a ${email}"; - static String m47(personName, email) => + static String m48(personName, email) => "Isso vinculará ${personName} a ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: 'sem memórias', one: '${formattedCount} memória', other: '${formattedCount} memórias')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m51(personName) => "Sem sugestões para ${personName}"; + static String m52(personName) => "Sem sugestões para ${personName}"; - static String m52(name) => "Não é ${name}?"; + static String m53(name) => "Não é ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m54(name) => "Festejando com ${name}"; + static String m55(name) => "Festejando com ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Força da senha: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Fale com o suporte ${providerName} se você foi cobrado"; - static String m57(name, age) => "${name} está com ${age}!"; + static String m58(name, age) => "${name} tem ${age} anos!"; - static String m58(name, age) => "${name} terá ${age} em breve"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + static String m59(name, age) => "${name} terá ${age} em breve"; static String m60(count) => + "${Intl.plural(count, zero: 'Sem fotos', one: '1 foto', other: '${count} fotos')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m61(endDate) => + static String m62(endDate) => "Avaliação grátis válida até ${endDate}.\nVocê pode alterar para um plano pago depois."; - static String m62(toEmail) => "Envie-nos um e-mail para ${toEmail}"; + static String m63(toEmail) => "Envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Envie os registros para \n${toEmail}"; + static String m64(toEmail) => "Envie os registros para \n${toEmail}"; - static String m64(name) => "Fazendo pose com ${name}"; + static String m65(name) => "Fazendo pose com ${name}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos no ${storeName}"; + static String m67(storeName) => "Avalie-nos no ${storeName}"; - static String m67(name) => "Atribuído a ${name}"; + static String m68(name) => "Atribuído a ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Você poderá acessar a conta após ${days} dias. Uma notificação será enviada para ${email}."; - static String m69(email) => + static String m70(email) => "Você pode recuperar a conta com e-mail ${email} por definir uma nova senha."; - static String m70(email) => "${email} está tentando recuperar sua conta."; + static String m71(email) => "${email} está tentando recuperar sua conta."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ambos os dois ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => - "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; + static String m73(userEmail) => + "${userEmail} será removido do álbum compartilhado\n\nQualquer foto adicionada por ele será removida."; - static String m73(endDate) => "Renovação de assinatura em ${endDate}"; + static String m74(endDate) => "Renovação de assinatura em ${endDate}"; - static String m74(name) => "Viajando de carro com ${name}"; + static String m75(name) => "Viajando de carro com ${name}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Incompatibilidade de comprimento de seções: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} selecionado(s)"; + static String m78(count) => "${count} selecionado(s)"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m78(name) => "Tirando selfies com ${name}"; + static String m80(name) => "Tirando selfies com ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; - static String m80(verificationID) => + static String m82(verificationID) => "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Código de referência do Ente: ${referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter ${referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m83(emailIDs) => "Compartilhado com ${emailIDs}"; + static String m85(emailIDs) => "Compartilhado com ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Este ${fileType} será excluído do dispositivo."; - static String m85(fileType) => + static String m87(fileType) => "Este ${fileType} está no Ente e em seu dispositivo."; - static String m86(fileType) => "Este ${fileType} será excluído do Ente."; + static String m88(fileType) => "Este ${fileType} será excluído do Ente."; - static String m87(name) => "Jogando esportes com ${name}"; + static String m89(name) => "Jogando esportes com ${name}"; - static String m88(name) => "Destacar ${name}"; + static String m90(name) => "Destacar ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m91(id) => + static String m93(id) => "Seu ${id} já está vinculado a outra conta Ente. Se você gostaria de usar seu ${id} com esta conta, entre em contato conosco\""; - static String m92(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m94(endDate) => "Sua assinatura será cancelada em ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} memórias preservadas"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Toque para enviar, atualmente o envio é ignorado devido a ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m96(email) => "Este é o ID de verificação de ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; - - static String m98(dateFormat) => "${dateFormat} com o passar dos anos"; + static String m98(email) => "Este é o ID de verificação de ${email}"; static String m99(count) => + "${Intl.plural(count, one: 'Esta semana, ${count} ano atrás', other: 'Esta semana, ${count} anos atrás')}"; + + static String m100(dateFormat) => "${dateFormat} com o passar dos anos"; + + static String m101(count) => "${Intl.plural(count, zero: 'Em breve', one: '1 dia', other: '${count} dias')}"; - static String m100(year) => "Viajem em ${year}"; + static String m102(year) => "Viajem em ${year}"; - static String m101(location) => "Viajem à ${location}"; + static String m103(location) => "Viajem à ${location}"; - static String m102(email) => + static String m104(email) => "Você foi convidado para ser um contato legado por ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "O tipo de galeria ${galleryType} não é suportado para renomear"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "O envio é ignorado devido a ${ignoreReason}"; - static String m105(count) => "Preservando ${count} memórias..."; + static String m107(count) => "Preservando ${count} memórias..."; - static String m106(endDate) => "Válido até ${endDate}"; + static String m108(endDate) => "Válido até ${endDate}"; - static String m107(email) => "Verificar ${email}"; + static String m109(email) => "Verificar ${email}"; - static String m108(count) => + static String m110(name) => "Visualizar ${name} para desvincular"; + + static String m111(count) => "${Intl.plural(count, zero: 'Adicionado 0 vizualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; - static String m109(email) => "Enviamos um e-mail à ${email}"; + static String m112(email) => "Enviamos um e-mail à ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m111(name) => "Você e ${name}"; + static String m114(name) => "Você e ${name}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -445,7 +449,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aplicar código"), "appstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"), - "archive": MessageLookupByLibrary.simpleMessage("Arquivo"), + "archive": MessageLookupByLibrary.simpleMessage("Arquive"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), "areYouSureThatYouWantToLeaveTheFamily": @@ -542,26 +546,10 @@ class MessageLookup extends MessageLookupByLibrary { "Cópia de segurança de vídeos"), "beach": MessageLookupByLibrary.simpleMessage("Areia e o mar"), "birthday": MessageLookupByLibrary.simpleMessage("Aniversário"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Promoção Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Editar todas as datas"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Limites de planos familiares"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Novo Ícone"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Memórias"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente."), "cachedData": MessageLookupByLibrary.simpleMessage("Dados armazenados em cache"), "calculating": MessageLookupByLibrary.simpleMessage("Calculando..."), @@ -616,8 +604,8 @@ class MessageLookup extends MessageLookupByLibrary { "checkingModels": MessageLookupByLibrary.simpleMessage("Verificando modelos..."), "city": MessageLookupByLibrary.simpleMessage("Na cidade"), - "claimFreeStorage": MessageLookupByLibrary.simpleMessage( - "Reivindicar armazenamento grátis"), + "claimFreeStorage": + MessageLookupByLibrary.simpleMessage("Reivindique armaz. grátis"), "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), "claimedStorageSoFar": m14, @@ -630,6 +618,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Clique"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Clique no menu adicional"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Fechar"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Agrupar por tempo de captura"), @@ -653,7 +643,7 @@ class MessageLookup extends MessageLookupByLibrary { "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( - "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado."), + "Colaboradores podem adicionar fotos e vídeos ao álbum compartilhado."), "collaboratorsSuccessfullyAdded": m16, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": @@ -749,7 +739,7 @@ class MessageLookup extends MessageLookupByLibrary { "delete": MessageLookupByLibrary.simpleMessage("Excluir"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( - "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar."), + "Lamentamos você ir. Compartilhe seu feedback para ajudar-nos a melhorar."), "deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage( "Excluir conta permanentemente"), "deleteAlbum": MessageLookupByLibrary.simpleMessage("Excluir álbum"), @@ -865,6 +855,7 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Editar"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Editar localização"), "editLocationTagTitle": @@ -879,16 +870,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail já registrado."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail não registrado."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("Enviar registros por e-mail"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contatos de emergência"), "empty": MessageLookupByLibrary.simpleMessage("Esvaziar"), @@ -896,9 +887,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Esvaziar a lixeira?"), "enable": MessageLookupByLibrary.simpleMessage("Ativar"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( - "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados"), + "Ente fornece aprendizado automático no dispositivo para reconhecimento facial, busca mágica e outros recursos de busca avançados."), "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( - "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial"), + "Ativar o aprendizado automático para busca mágica e reconhecimento facial"), "enableMaps": MessageLookupByLibrary.simpleMessage("Ativar mapas"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( "Isso exibirá suas fotos em um mapa mundial.\n\nEste mapa é hospedado por Open Street Map, e as exatas localizações das fotos nunca serão compartilhadas.\n\nVocê pode desativar esta função a qualquer momento em Opções."), @@ -920,7 +911,7 @@ class MessageLookup extends MessageLookupByLibrary { "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( - "Sua família também pode ser adicionada ao seu plano."), + "Sua família também poderá ser adicionada ao seu plano."), "enterAlbumName": MessageLookupByLibrary.simpleMessage("Inserir nome do álbum"), "enterCode": MessageLookupByLibrary.simpleMessage("Insira o código"), @@ -949,6 +940,8 @@ class MessageLookup extends MessageLookupByLibrary { "Insira um endereço de e-mail válido."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "Insira seu endereço de e-mail"), + "enterYourNewEmailAddress": + MessageLookupByLibrary.simpleMessage("Insira seu novo e-mail"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Insira sua senha"), "enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -967,7 +960,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Rosto não agrupado ainda, volte aqui mais tarde"), "faceRecognition": @@ -1006,7 +999,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "faqs": MessageLookupByLibrary.simpleMessage("Perguntas frequentes"), "favorite": MessageLookupByLibrary.simpleMessage("Favorito"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "file": MessageLookupByLibrary.simpleMessage("Arquivo"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1020,8 +1013,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), "filesSavedToGallery": @@ -1031,35 +1024,35 @@ class MessageLookup extends MessageLookupByLibrary { "findThemQuickly": MessageLookupByLibrary.simpleMessage("Busque-os rapidamente"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"), - "food": MessageLookupByLibrary.simpleMessage("Prazer em culinária"), + "food": MessageLookupByLibrary.simpleMessage("Amor por culinária"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para suas memórias"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Esqueci a senha"), "foundFaces": MessageLookupByLibrary.simpleMessage("Rostos encontrados"), - "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( - "Armazenamento grátis reivindicado"), - "freeStorageOnReferralSuccess": m35, + "freeStorageClaimed": + MessageLookupByLibrary.simpleMessage("Armaz. grátis reivindicado"), + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Armazenamento disponível"), "freeTrial": MessageLookupByLibrary.simpleMessage("Avaliação grátis"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espaço"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galeria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Até 1.000 memórias exibidas na galeria"), "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Gerando chaves de criptografia..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir às opções"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID do Google Play"), @@ -1073,6 +1066,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Vista do convidado"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Para ativar a vista do convidado, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Não rastreamos instalações de aplicativo. Seria útil se você contasse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1088,7 +1083,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Ocultar itens compartilhados da galeria inicial"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hospedado em OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Como funciona"), @@ -1145,7 +1140,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens exibem o número de dias restantes antes da exclusão permanente"), @@ -1166,7 +1161,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Ajude-nos com esta informação"), "language": MessageLookupByLibrary.simpleMessage("Idioma"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Última atualização"), "lastYearsTrip": @@ -1181,7 +1176,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Contas legadas"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "O legado permite que contatos confiáveis acessem sua conta em sua ausência."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1198,21 +1193,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("para compartilhar rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nunca"), "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular pessoa"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( - "para melhor experiência de compartilhamento"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "para melhorar o compartilhamento"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Você pode compartilhar sua assinatura com seus familiares"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Nós preservamos mais de 30 milhões de memórias até então"), + "Preservamos mais de 200 milhões de memórias até então"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1269,6 +1264,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Mantenha pressionado em um item para visualizá-lo em tela cheia"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Repetir vídeo desativado"), "loopVideoOn": @@ -1276,7 +1273,7 @@ class MessageLookup extends MessageLookupByLibrary { "lostDevice": MessageLookupByLibrary.simpleMessage("Perdeu o dispositivo?"), "machineLearning": - MessageLookupByLibrary.simpleMessage("Aprendizagem automática"), + MessageLookupByLibrary.simpleMessage("Aprendizado automático"), "magicSearch": MessageLookupByLibrary.simpleMessage("Busca mágica"), "magicSearchHint": MessageLookupByLibrary.simpleMessage( "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. \'flor\', \'carro vermelho\', \'identidade\'"), @@ -1298,23 +1295,23 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Eu"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Juntar com o existente"), "mergedPhotos": MessageLookupByLibrary.simpleMessage("Fotos mescladas"), "mlConsent": MessageLookupByLibrary.simpleMessage( - "Ativar aprendizagem automática"), - "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( - "Eu entendo, e desejo ativar a aprendizagem automática"), + "Ativar o aprendizado automático"), + "mlConsentConfirmation": + MessageLookupByLibrary.simpleMessage("Concordo e desejo ativá-lo"), "mlConsentDescription": MessageLookupByLibrary.simpleMessage( - "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta."), + "Se ativar o aprendizado automático, Ente extrairá informações de geometria facial dos arquivos, incluindo aqueles compartilhados consigo.\n\nIsso acontecerá em seu dispositivo, e qualquer informação biométrica gerada será criptografada de ponta a ponta."), "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( "Clique aqui para mais detalhes sobre este recurso na política de privacidade"), "mlConsentTitle": MessageLookupByLibrary.simpleMessage( - "Ativar aprendizagem automática?"), + "Ativar aprendizado automático?"), "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."), + "Saiba que o aprendizado automático afetará a bateria do dispositivo negativamente até todos os itens serem indexados. Utilize a versão para computadores para melhor indexação, todos os resultados se auto-sincronizaram."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Celular, Web, Computador"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderado"), @@ -1329,14 +1326,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Mais recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mais relevante"), "mountains": MessageLookupByLibrary.simpleMessage("Sob as montanhas"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Mover fotos selecionadas para uma data"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para o álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover ao álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1351,6 +1348,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Novo álbum"), "newLocation": MessageLookupByLibrary.simpleMessage("Nova localização"), "newPerson": MessageLookupByLibrary.simpleMessage("Nova pessoa"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Novo intervalo"), "newToEnte": MessageLookupByLibrary.simpleMessage("Novo no Ente"), "newest": MessageLookupByLibrary.simpleMessage("Mais recente"), @@ -1390,10 +1388,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nenhum bloqueio do sistema encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Não é esta pessoa?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1405,8 +1403,12 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "No ente"), - "onTheRoad": MessageLookupByLibrary.simpleMessage("Na estrada de novo"), - "onlyFamilyAdminCanChangeCode": m53, + "onTheRoad": + MessageLookupByLibrary.simpleMessage("Na estrada novamente"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), "oops": MessageLookupByLibrary.simpleMessage("Ops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1436,7 +1438,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Pareamento concluído"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Verificação pendente"), "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), @@ -1447,7 +1449,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueio por senha"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas."), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1458,7 +1460,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1471,21 +1473,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Excluir permanentemente"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Excluir permanentemente do dispositivo?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Nome da pessoa"), - "personTurningAge": m58, - "pets": MessageLookupByLibrary.simpleMessage("Companhia de pelos"), + "personTurningAge": m59, + "pets": MessageLookupByLibrary.simpleMessage("Companhias peludas"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Descrições das fotos"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Tamanho da grade de fotos"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotos"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Suas fotos adicionadas serão removidas do álbum"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "As fotos mantêm a diferença de tempo relativo"), @@ -1497,7 +1499,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Reproduzir original"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Reproduzir transmissão"), "playstoreSubscription": @@ -1511,14 +1513,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Registre-se novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1531,7 +1533,7 @@ class MessageLookup extends MessageLookupByLibrary { "Por favor, aguarde mais algum tempo antes de tentar novamente"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Aguarde um pouco, isso talvez leve um tempo."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparando registros..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preservar mais"), @@ -1546,11 +1548,11 @@ class MessageLookup extends MessageLookupByLibrary { "privateBackups": MessageLookupByLibrary.simpleMessage("Cópias privadas"), "privateSharing": - MessageLookupByLibrary.simpleMessage("Compartilhamento privado"), + MessageLookupByLibrary.simpleMessage("Compartilha privada"), "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Processado"), "processing": MessageLookupByLibrary.simpleMessage("Processando"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Processando vídeos"), "publicLinkCreated": @@ -1564,11 +1566,13 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), "rateUs": MessageLookupByLibrary.simpleMessage("Avaliar"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Reatribuir \"Eu\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reatribuindo..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1577,7 +1581,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar conta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("A recuperação iniciou"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chave de recuperação"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1592,12 +1596,12 @@ class MessageLookup extends MessageLookupByLibrary { "Chave de recuperação verificada"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperação com sucesso!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Um contato confiável está tentando acessar sua conta"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar)."), "recreatePasswordTitle": @@ -1612,7 +1616,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles então se inscrevem num plano pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "As referências estão atualmente pausadas"), @@ -1641,7 +1645,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1661,7 +1665,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Informar um erro"), "reportBug": MessageLookupByLibrary.simpleMessage("Informar erro"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), @@ -1686,7 +1690,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Revisar sugestões"), "right": MessageLookupByLibrary.simpleMessage("Direita"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Girar"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Girar para a esquerda"), @@ -1744,8 +1748,8 @@ class MessageLookup extends MessageLookupByLibrary { "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver links de álbum compartilhado no aplicativo"), @@ -1794,9 +1798,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão removidos desta pessoa, entretanto não serão excluídos da sua biblioteca."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1826,16 +1830,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários não ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Compartilhar seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1848,7 +1852,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), "sharedWithYou": @@ -1867,11 +1871,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sair em outros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), "someItemsAreInBothEnteAndYourDevice": @@ -1889,6 +1893,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Algo deu errado. Tente outra vez"), "sorry": MessageLookupByLibrary.simpleMessage("Desculpe"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Desculpe, não podemos fazer cópia de segurança deste arquivo no momento, nós tentaremos mais tarde."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Desculpe, não foi possível adicionar aos favoritos!"), "sorryCouldNotRemoveFromFavorites": @@ -1900,6 +1906,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\ninicie sessão com um dispositivo diferente."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordenar"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordenar por"), "sortNewestFirst": @@ -1907,8 +1915,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Antigos primeiro"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Sucesso"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Destacar si mesmo"), "startAccountRecoveryTitle": @@ -1923,15 +1931,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Detalhes da transmissão"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Inscrever-se"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Você precisa de uma inscrição paga ativa para ativar o compartilhamento."), @@ -1949,7 +1957,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sugerir recurso"), "sunrise": MessageLookupByLibrary.simpleMessage("No horizonte"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1961,7 +1969,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Toque para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toque para enviar"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe."), "terminate": MessageLookupByLibrary.simpleMessage("Encerrar"), @@ -1983,7 +1991,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Eles serão excluídos de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2001,12 +2009,12 @@ class MessageLookup extends MessageLookupByLibrary { "Esta imagem não possui dados EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Este é você!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage( "Esta semana com o passar dos anos"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Isso fará você sair do dispositivo a seguir:"), @@ -2018,7 +2026,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Isto removerá links públicos de todos os links rápidos selecionados."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema."), @@ -2032,13 +2040,13 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Recortar"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatos confiáveis"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente."), @@ -2057,7 +2065,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores redefinida com sucesso"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), @@ -2080,10 +2088,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Enviando arquivos para o álbum..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -2101,7 +2109,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço usado"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação. Tente novamente"), @@ -2109,7 +2117,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -2122,7 +2130,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Informações do vídeo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vídeo"), "videoStreaming": - MessageLookupByLibrary.simpleMessage("Transmissão de vídeo"), + MessageLookupByLibrary.simpleMessage("Vídeos transmissíveis"), "videos": MessageLookupByLibrary.simpleMessage("Vídeos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Ver sessões ativas"), @@ -2136,10 +2144,11 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( "Ver arquivos que consumem a maior parte do armazenamento."), "viewLogs": MessageLookupByLibrary.simpleMessage("Ver registros"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver chave de recuperação"), "viewer": MessageLookupByLibrary.simpleMessage("Visualizador"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Visite o web.ente.io para gerenciar sua assinatura"), "waitingForVerification": @@ -2152,7 +2161,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), @@ -2161,7 +2170,7 @@ class MessageLookup extends MessageLookupByLibrary { "Um contato confiável pode ajudá-lo em recuperar seus dados."), "yearShort": MessageLookupByLibrary.simpleMessage("ano"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2176,7 +2185,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Sim, redefinir pessoa"), "you": MessageLookupByLibrary.simpleMessage("Você"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage( "Você está em um plano familiar!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2192,10 +2201,10 @@ class MessageLookup extends MessageLookupByLibrary { "youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage( "Você não pode rebaixar para este plano"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage( - "Você não pode compartilhar consigo mesmo"), + "Não é possível compartilhar consigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_pt_PT.dart b/mobile/lib/generated/intl/messages_pt_PT.dart index 8b27132a07..3c17d44eb8 100644 --- a/mobile/lib/generated/intl/messages_pt_PT.dart +++ b/mobile/lib/generated/intl/messages_pt_PT.dart @@ -79,136 +79,136 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m27(newEmail) => "Email alterado para ${newEmail}"; + static String m28(newEmail) => "Email alterado para ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m31(text) => "Fotos extras encontradas para ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; + static String m32(text) => "Fotos extras encontradas para ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste álbum teve um backup seguro"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código"; - static String m36(endDate) => "Teste gratuito válido até ${endDate}"; + static String m37(endDate) => "Teste gratuito válido até ${endDate}"; - static String m38(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Libertar ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m45(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; - static String m50(albumName) => "Movido com sucesso para ${albumName}"; + static String m51(albumName) => "Movido com sucesso para ${albumName}"; - static String m52(name) => "Não é ${name}?"; + static String m53(name) => "Não é ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Força da palavra-passe: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m61(endDate) => + static String m62(endDate) => "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m62(toEmail) => + static String m63(toEmail) => "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m63(toEmail) => "Por favor, envie os logs para \n${toEmail}"; + static String m64(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m65(folderName) => "Processando ${folderName}..."; + static String m66(folderName) => "Processando ${folderName}..."; - static String m66(storeName) => "Avalie-nos em ${storeName}"; + static String m67(storeName) => "Avalie-nos em ${storeName}"; - static String m71(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; + static String m72(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum"; - static String m73(endDate) => "A subscrição é renovada em ${endDate}"; + static String m74(endDate) => "A subscrição é renovada em ${endDate}"; + + static String m76(count) => + "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; + + static String m78(count) => "${count} selecionado(s)"; + + static String m79(count, yourCount) => + "${count} selecionado(s) (${yourCount} seus)"; + + static String m81(verificationID) => + "Aqui está o meu ID de verificação: ${verificationID} para ente.io."; + + static String m82(verificationID) => + "Ei, você pode confirmar que este é seu ID de verificação do ente.io: ${verificationID}"; + + static String m83(referralCode, referralStorageInGB) => + "Insira o código de referência: ${referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io"; + + static String m84(numberOfPeople) => + "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; + + static String m85(emailIDs) => "Partilhado com ${emailIDs}"; + + static String m86(fileType) => + "Este ${fileType} será eliminado do seu dispositivo."; + + static String m87(fileType) => + "Este ${fileType} encontra-se tanto no Ente como no seu dispositivo."; + + static String m88(fileType) => "Este ${fileType} será eliminado do Ente."; + + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( + usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => + "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; + + static String m93(id) => + "Seu ${id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; + + static String m94(endDate) => "A sua subscrição será cancelada em ${endDate}"; + + static String m95(completed, total) => + "${completed}/${total} memórias preservadas"; + + static String m97(storageAmountInGB) => + "Eles também recebem ${storageAmountInGB} GB"; + + static String m98(email) => "Este é o ID de verificação de ${email}"; + + static String m101(count) => + "${Intl.plural(count, zero: 'Brevemente', one: '1 dia', other: '${count} dias')}"; + + static String m105(galleryType) => + "Tipo de galeria ${galleryType} não é permitido para renomear"; + + static String m106(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; + + static String m107(count) => "Preservar ${count} memórias..."; + + static String m108(endDate) => "Válido até ${endDate}"; + + static String m109(email) => "Verificar e-mail"; + + static String m112(email) => + "Enviamos um e-mail para ${email}"; static String m113(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m76(count) => "${count} selecionado(s)"; - - static String m77(count, yourCount) => - "${count} selecionado(s) (${yourCount} seus)"; - - static String m79(verificationID) => - "Aqui está o meu ID de verificação: ${verificationID} para ente.io."; - - static String m80(verificationID) => - "Ei, você pode confirmar que este é seu ID de verificação do ente.io: ${verificationID}"; - - static String m81(referralCode, referralStorageInGB) => - "Insira o código de referência: ${referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io"; - - static String m82(numberOfPeople) => - "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - - static String m83(emailIDs) => "Partilhado com ${emailIDs}"; - - static String m84(fileType) => - "Este ${fileType} será eliminado do seu dispositivo."; - - static String m85(fileType) => - "Este ${fileType} encontra-se tanto no Ente como no seu dispositivo."; - - static String m86(fileType) => "Este ${fileType} será eliminado do Ente."; - - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; - - static String m90( - usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => - "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - - static String m91(id) => - "Seu ${id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; - - static String m92(endDate) => "A sua subscrição será cancelada em ${endDate}"; - - static String m93(completed, total) => - "${completed}/${total} memórias preservadas"; - - static String m95(storageAmountInGB) => - "Eles também recebem ${storageAmountInGB} GB"; - - static String m96(email) => "Este é o ID de verificação de ${email}"; - - static String m99(count) => - "${Intl.plural(count, zero: 'Brevemente', one: '1 dia', other: '${count} dias')}"; - - static String m103(galleryType) => - "Tipo de galeria ${galleryType} não é permitido para renomear"; - - static String m104(ignoreReason) => "Envio ignorado devido à ${ignoreReason}"; - - static String m105(count) => "Preservar ${count} memórias..."; - - static String m106(endDate) => "Válido até ${endDate}"; - - static String m107(email) => "Verificar e-mail"; - - static String m109(email) => - "Enviamos um e-mail para ${email}"; - - static String m110(count) => - "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - - static String m112(storageSaved) => + static String m115(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -412,6 +412,7 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage( "Cópia de segurança de vídeos"), "birthday": MessageLookupByLibrary.simpleMessage("Aniversário"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Promoção Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), @@ -473,6 +474,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("Clique"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Clique no menu adicional"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Fechar"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Agrupar por tempo de captura"), @@ -710,8 +713,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edições para localização só serão vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação por e-mail"), "emailYourLogs": @@ -790,7 +793,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar os seus dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconhecimento facial"), "faces": MessageLookupByLibrary.simpleMessage("Rostos"), @@ -836,8 +839,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos apagados"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -855,12 +858,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Armazenamento gratuito reclamado"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Armazenamento livre utilizável"), "freeTrial": MessageLookupByLibrary.simpleMessage("Teste grátis"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Libertar espaço no dispositivo"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -871,7 +874,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Gerando chaves de encriptação..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir para as definições"), "googlePlayId": @@ -885,6 +888,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Visão de convidado"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Para ativar a vista de convidado, configure o código de acesso do dispositivo ou o bloqueio do ecrã nas definições do sistema."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Não monitorizamos as instalações de aplicações. Ajudaria se nos dissesse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -949,7 +954,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens mostram o número de dias restantes antes da eliminação permanente"), @@ -979,7 +984,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limite de dispositivo"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expirado"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), @@ -988,8 +993,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Fotos Em Tempo Real"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Pode partilhar a sua subscrição com a sua família"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Nós preservamos mais de 30 milhões de memórias até agora"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1047,6 +1050,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Pressione e segure em um item para ver em tela cheia"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Repetir vídeo desligado"), "loopVideoOn": @@ -1103,7 +1108,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum oculto"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Mover para o lixo"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1117,6 +1122,7 @@ class MessageLookup extends MessageLookupByLibrary { "never": MessageLookupByLibrary.simpleMessage("Nunca"), "newAlbum": MessageLookupByLibrary.simpleMessage("Novo álbum"), "newPerson": MessageLookupByLibrary.simpleMessage("Nova pessoa"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Novo no Ente"), "newest": MessageLookupByLibrary.simpleMessage("Recentes"), "next": MessageLookupByLibrary.simpleMessage("Seguinte"), @@ -1153,7 +1159,7 @@ class MessageLookup extends MessageLookupByLibrary { "Não foram encontrados resultados"), "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nenhum bloqueio de sistema encontrado"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Ainda nada partilhado consigo"), "nothingToSeeHere": @@ -1163,7 +1169,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Em ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1197,7 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary { "Palavra-passe alterada com sucesso"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueio da palavra-passe"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1208,7 +1217,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("O pagamento falhou"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1237,7 +1246,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Bloqueio por PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Subscrição da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1249,14 +1258,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inicie sessão novamente"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecione links rápidos para remover"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1283,7 +1292,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Backups privados"), "privateSharing": MessageLookupByLibrary.simpleMessage("Partilha privada"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link público criado"), "publicLinkEnabled": @@ -1293,7 +1302,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Abrir ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Avaliar aplicação"), "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1329,7 +1340,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Envie este código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles se inscrevem em um plano pago"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "As referências estão atualmente em pausa"), @@ -1355,7 +1366,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1373,7 +1384,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar subscrição"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Reporte um bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), @@ -1448,7 +1459,7 @@ class MessageLookup extends MessageLookupByLibrary { "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Convide pessoas e verá todas as fotos partilhadas por elas aqui"), - "searchResultCount": m113, + "searchResultCount": m76, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecione uma localização"), @@ -1476,8 +1487,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1508,16 +1519,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Partilhar um álbum"), "shareLink": MessageLookupByLibrary.simpleMessage("Partilhar link"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partilhar apenas com as pessoas que deseja"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários que não usam Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partilhe o seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1530,7 +1541,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos partilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partilhado comigo"), "sharedWithYou": @@ -1547,11 +1558,11 @@ class MessageLookup extends MessageLookupByLibrary { "Terminar a sessão noutros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e política de privacidade"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Será eliminado de todos os álbuns."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1580,6 +1591,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\npor favor iniciar sessão com um dispositivo diferente."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Ordenar"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Ordenar por"), "sortNewestFirst": @@ -1597,13 +1610,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Subscrever"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Você precisa de uma assinatura paga ativa para ativar o compartilhamento."), @@ -1620,7 +1633,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir recursos"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1649,7 +1662,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão eliminados do seu dispositivo."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Serão eliminados de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1665,7 +1678,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Este email já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagem não tem dados exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1689,7 +1702,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixo"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Cortar"), "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( @@ -1709,7 +1722,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores redefinida com êxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), @@ -1732,10 +1745,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Enviar ficheiros para o álbum..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservar 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1751,7 +1764,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço utilizado"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação, por favor tente novamente"), @@ -1759,7 +1772,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -1797,13 +1810,13 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que ainda não possui"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), "whatsNew": MessageLookupByLibrary.simpleMessage("O que há de novo"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1836,7 +1849,7 @@ class MessageLookup extends MessageLookupByLibrary { "Não podes partilhar contigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("A sua conta foi eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_ro.dart b/mobile/lib/generated/intl/messages_ro.dart index e63f61766f..6a0cb32a45 100644 --- a/mobile/lib/generated/intl/messages_ro.dart +++ b/mobile/lib/generated/intl/messages_ro.dart @@ -80,162 +80,162 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} fișiere, ${formattedSize} fiecare"; - static String m27(newEmail) => "E-mail modificat în ${newEmail}"; + static String m28(newEmail) => "E-mail modificat în ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} nu are un cont Ente.\n\nTrimiteți-le o invitație pentru a distribui fotografii."; - static String m31(text) => "S-au găsit fotografii extra pentru ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'Un fișier de pe acest dispozitiv a fost deja salvat în siguranță', few: '${formattedNumber} fișiere de pe acest dispozitiv au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță')}"; + static String m32(text) => "S-au găsit fotografii extra pentru ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: 'Un fișier de pe acest dispozitiv a fost deja salvat în siguranță', few: '${formattedNumber} fișiere de pe acest dispozitiv au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță')}"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: 'Un fișier din acest album a fost deja salvat în siguranță', few: '${formattedNumber} fișiere din acest album au fost deja salvate în siguranță', other: '${formattedNumber} de fișiere din acest album au fost deja salvate în siguranță')}"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB de fiecare dată când cineva se înscrie pentru un plan plătit și aplică codul dvs."; - static String m36(endDate) => + static String m37(endDate) => "Perioadă de încercare valabilă până pe ${endDate}"; - static String m38(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Se procesează ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} articol', few: '${count} articole', other: '${count} de articole')}"; - static String m44(email) => + static String m45(email) => "${email} v-a invitat să fiți un contact de încredere"; - static String m45(expiryTime) => "Linkul va expira pe ${expiryTime}"; + static String m46(expiryTime) => "Linkul va expira pe ${expiryTime}"; - static String m50(albumName) => "S-au mutat cu succes în ${albumName}"; + static String m51(albumName) => "S-au mutat cu succes în ${albumName}"; - static String m51(personName) => "Nicio sugestie pentru ${personName}"; + static String m52(personName) => "Nicio sugestie pentru ${personName}"; - static String m52(name) => "Nu este ${name}?"; + static String m53(name) => "Nu este ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Vă rugăm să contactați ${familyAdminEmail} pentru a vă schimba codul."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Complexitatea parolei: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Vă rugăm să vorbiți cu asistența ${providerName} dacă ați fost taxat"; - static String m61(endDate) => + static String m62(endDate) => "Perioada de încercare gratuită valabilă până pe ${endDate}.\nUlterior, puteți opta pentru un plan plătit."; - static String m62(toEmail) => + static String m63(toEmail) => "Vă rugăm să ne trimiteți un e-mail la ${toEmail}"; - static String m63(toEmail) => + static String m64(toEmail) => "Vă rugăm să trimiteți jurnalele la \n${toEmail}"; - static String m65(folderName) => "Se procesează ${folderName}..."; + static String m66(folderName) => "Se procesează ${folderName}..."; - static String m66(storeName) => "Evaluați-ne pe ${storeName}"; + static String m67(storeName) => "Evaluați-ne pe ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Puteți accesa contul după ${days} zile. O notificare va fi trimisă la ${email}."; - static String m69(email) => + static String m70(email) => "Acum puteți recupera contul ${email} setând o nouă parolă."; - static String m70(email) => "${email} încearcă să vă recupereze contul."; + static String m71(email) => "${email} încearcă să vă recupereze contul."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Amândoi primiți ${storageInGB} GB* gratuit"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} va fi eliminat din acest album distribuit\n\nOrice fotografii adăugate de acesta vor fi, de asemenea, eliminate din album"; - static String m73(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; + static String m74(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} rezultat găsit', few: '${count} rezultate găsite', other: '${count} de rezultate găsite')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Lungimea secțiunilor nu se potrivesc: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} selectate"; + static String m78(count) => "${count} selectate"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} selectate (${yourCount} ale dvs.)"; - static String m79(verificationID) => + static String m81(verificationID) => "Acesta este ID-ul meu de verificare: ${verificationID} pentru ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Poți confirma că acesta este ID-ul tău de verificare ente.io: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Codul de recomandare Ente: ${referralCode}\n\nAplică-l în Setări → General → Recomandări pentru a obține ${referralStorageInGB} GB gratuit după ce te înscrii pentru un plan plătit\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Distribuiți cu anumite persoane', one: 'Distribuit cu o persoană', other: 'Distribuit cu ${numberOfPeople} de persoane')}"; - static String m83(emailIDs) => "Distribuit cu ${emailIDs}"; - - static String m84(fileType) => - "Fișierul de tip ${fileType} va fi șters din dispozitivul dvs."; - - static String m85(fileType) => - "Fișierul de tip ${fileType} este atât în Ente, cât și în dispozitivul dvs."; + static String m85(emailIDs) => "Distribuit cu ${emailIDs}"; static String m86(fileType) => + "Fișierul de tip ${fileType} va fi șters din dispozitivul dvs."; + + static String m87(fileType) => + "Fișierul de tip ${fileType} este atât în Ente, cât și în dispozitivul dvs."; + + static String m88(fileType) => "Fișierul de tip ${fileType} va fi șters din Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} din ${totalAmount} ${totalStorageUnit} utilizat"; - static String m91(id) => + static String m93(id) => "${id} este deja legat la un alt cont Ente.\nDacă doriți să folosiți ${id} cu acest cont, vă rugăm să contactați asistența noastră"; - static String m92(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; + static String m94(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} amintiri salvate"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Atingeți pentru a încărca, încărcarea este ignorată în prezent datorită ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "De asemenea, va primii ${storageAmountInGB} GB"; - static String m96(email) => "Acesta este ID-ul de verificare al ${email}"; + static String m98(email) => "Acesta este ID-ul de verificare al ${email}"; - static String m99(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Curând', one: 'O zi', other: '${count} de zile')}"; - static String m102(email) => + static String m104(email) => "Ați fost învitat să fiți un contact de moștenire de către ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Tipul de galerie ${galleryType} nu este acceptat pentru redenumire"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Încărcare ignorată din motivul ${ignoreReason}"; - static String m105(count) => "Se salvează ${count} amintiri..."; + static String m107(count) => "Se salvează ${count} amintiri..."; - static String m106(endDate) => "Valabil până pe ${endDate}"; + static String m108(endDate) => "Valabil până pe ${endDate}"; - static String m107(email) => "Verificare ${email}"; + static String m109(email) => "Verificare ${email}"; - static String m109(email) => "Am trimis un e-mail la ${email}"; + static String m112(email) => "Am trimis un e-mail la ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: 'acum ${count} an', few: 'acum ${count} ani', other: 'acum ${count} de ani')}"; - static String m112(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; + static String m115(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -452,6 +452,7 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage( "Copie de rezervă videoclipuri"), "birthday": MessageLookupByLibrary.simpleMessage("Ziua de naștere"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Ofertă Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), @@ -528,6 +529,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Apăsați"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Apăsați pe meniul suplimentar"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Închidere"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Grupare după timpul capturării"), @@ -771,8 +774,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail deja înregistrat."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-mailul nu este înregistrat."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( @@ -859,7 +862,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export de date"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("S-au găsit fotografii extra"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Fața nu este încă grupată, vă rugăm să reveniți mai târziu"), "faceRecognition": @@ -910,8 +913,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipuri de fișiere"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage( "Tipuri de fișiere și denumiri"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fișiere șterse"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Fișiere salvate în galerie"), @@ -926,13 +929,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("S-au găsit fețe"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spațiu gratuit revendicat"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spațiu gratuit utilizabil"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Perioadă de încercare gratuită"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Eliberați spațiu pe dispozitiv"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -944,7 +947,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Se generează cheile de criptare..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Mergeți la setări"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), @@ -957,6 +960,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Mod oaspete"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Pentru a activa modul oaspete, vă rugăm să configurați codul de acces al dispozitivului sau blocarea ecranului în setările sistemului."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Nu urmărim instalările aplicației. Ne-ar ajuta dacă ne-ați spune unde ne-ați găsit!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1028,7 +1033,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Se pare că ceva nu a mers bine. Vă rugăm să încercați din nou după ceva timp. Dacă eroarea persistă, vă rugăm să contactați echipa noastră de asistență."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Articolele afișează numărul de zile rămase până la ștergerea definitivă"), @@ -1060,7 +1065,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Moștenire"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Conturi de moștenire"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Moștenirea permite contactelor de încredere să vă acceseze contul în absența dvs."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1073,7 +1078,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limită de dispozitive"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirat"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expirarea linkului"), "linkHasExpired": @@ -1082,8 +1087,6 @@ class MessageLookup extends MessageLookupByLibrary { "livePhotos": MessageLookupByLibrary.simpleMessage("Fotografii live"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puteți împărți abonamentul cu familia dvs."), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Am păstrat până acum peste 30 de milioane de amintiri"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Păstrăm 3 copii ale datelor dvs., dintre care una într-un adăpost antiatomic subteran"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1140,6 +1143,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Apăsați lung pe un articol pentru a-l vizualiza pe tot ecranul"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Repetare video dezactivată"), "loopVideoOn": @@ -1203,7 +1208,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mutare în album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mutați în albumul ascuns"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("S-a mutat în coșul de gunoi"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1219,6 +1224,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Album nou"), "newLocation": MessageLookupByLibrary.simpleMessage("Locație nouă"), "newPerson": MessageLookupByLibrary.simpleMessage("Persoană nouă"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Nou la Ente"), "newest": MessageLookupByLibrary.simpleMessage("Cele mai noi"), "next": MessageLookupByLibrary.simpleMessage("Înainte"), @@ -1255,10 +1261,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Niciun rezultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nu s-au găsit rezultate"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nu s-a găsit nicio blocare de sistem"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nimic distribuit cu dvs. încă"), "nothingToSeeHere": @@ -1268,7 +1274,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Pe dispozitiv"), "onEnte": MessageLookupByLibrary.simpleMessage( "Pe ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Numai el/ea"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1307,7 +1316,7 @@ class MessageLookup extends MessageLookupByLibrary { "Parola a fost schimbată cu succes"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blocare cu parolă"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Puterea parolei este calculată luând în considerare lungimea parolei, caracterele utilizate și dacă parola apare sau nu în top 10.000 cele mai utilizate parole"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1318,7 +1327,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Plata nu a reușit"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Din păcate, plata dvs. nu a reușit. Vă rugăm să contactați asistență și vom fi bucuroși să vă ajutăm!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Elemente în așteptare"), "pendingSync": @@ -1347,7 +1356,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixați albumul"), "pinLock": MessageLookupByLibrary.simpleMessage("Blocare PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Redare album pe TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonament PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1359,14 +1368,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vă rugăm să contactați asistența dacă problema persistă"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Vă rugăm să acordați permisiuni"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Vă rugăm, autentificați-vă din nou"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Vă rugăm să selectați linkurile rapide de eliminat"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Vă rugăm să încercați din nou"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1396,7 +1405,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Distribuire privată"), "proceed": MessageLookupByLibrary.simpleMessage("Continuați"), "processed": MessageLookupByLibrary.simpleMessage("Procesate"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link public creat"), "publicLinkEnabled": @@ -1408,7 +1417,9 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evaluați aplicația"), "rateUs": MessageLookupByLibrary.simpleMessage("Evaluați-ne"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Recuperare"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperare cont"), @@ -1417,7 +1428,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperare cont"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperare inițiată"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Cheie de recuperare"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1432,12 +1443,12 @@ class MessageLookup extends MessageLookupByLibrary { "Cheie de recuperare verificată"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Cheia dvs. de recuperare este singura modalitate de a vă recupera fotografiile dacă uitați parola. Puteți găsi cheia dvs. de recuperare în Setări > Cont.\n\nVă rugăm să introduceți aici cheia de recuperare pentru a verifica dacă ați salvat-o corect."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperare reușită!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de încredere încearcă să vă acceseze contul"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Dispozitivul actual nu este suficient de puternic pentru a vă verifica parola, dar o putem regenera într-un mod care să funcționeze cu toate dispozitivele.\n\nVă rugăm să vă conectați utilizând cheia de recuperare și să vă regenerați parola (dacă doriți, o puteți utiliza din nou pe aceeași)."), "recreatePasswordTitle": @@ -1453,7 +1464,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dați acest cod prietenilor"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Aceștia se înscriu la un plan cu plată"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Recomandări"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Recomandările sunt momentan întrerupte"), @@ -1485,7 +1496,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminați linkul"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Eliminați participantul"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminați eticheta persoanei"), "removePublicLink": @@ -1506,7 +1517,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Redenumiți fișierul"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Reînnoire abonament"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Raportați o eroare"), "reportBug": MessageLookupByLibrary.simpleMessage("Raportare eroare"), @@ -1587,8 +1598,8 @@ class MessageLookup extends MessageLookupByLibrary { "Invitați persoane și veți vedea aici toate fotografiile distribuite de acestea"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Persoanele vor fi afișate aici odată ce procesarea și sincronizarea este completă"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Securitate"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Vedeți linkurile albumelor publice în aplicație"), @@ -1623,8 +1634,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Articolele selectate vor fi șterse din toate albumele și mutate în coșul de gunoi."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Trimitere"), "sendEmail": MessageLookupByLibrary.simpleMessage("Trimiteți e-mail"), "sendInvite": @@ -1657,16 +1668,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Distribuiți un album acum"), "shareLink": MessageLookupByLibrary.simpleMessage("Distribuiți linkul"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Distribuiți numai cu persoanele pe care le doriți"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarcă Ente pentru a putea distribui cu ușurință fotografii și videoclipuri în calitate originală\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Distribuiți cu utilizatori din afara Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Distribuiți primul album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1679,7 +1690,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Fotografii partajate noi"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Primiți notificări atunci când cineva adaugă o fotografie la un album distribuit din care faceți parte"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Distribuit mie"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Distribuite cu dvs."), @@ -1695,11 +1706,11 @@ class MessageLookup extends MessageLookupByLibrary { "Deconectați alte dispozitive"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Sunt de acord cu termenii de prestare ai serviciului și politica de confidențialitate"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Acesta va fi șters din toate albumele."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Omiteți"), "social": MessageLookupByLibrary.simpleMessage("Rețele socializare"), "someItemsAreInBothEnteAndYourDevice": @@ -1728,6 +1739,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Ne pare rău, nu am putut genera chei securizate pe acest dispozitiv.\n\nvă rugăm să vă înregistrați de pe un alt dispozitiv."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sortare"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sortare după"), "sortNewestFirst": @@ -1747,13 +1760,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Spațiu"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Dvs."), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limita de spațiu depășită"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Puternică"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abonare"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Aveți nevoie de un abonament plătit activ pentru a activa distribuirea."), @@ -1770,7 +1783,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerați funcționalități"), "support": MessageLookupByLibrary.simpleMessage("Asistență"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizare oprită"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizare..."), @@ -1783,7 +1796,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atingeți pentru a debloca"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Atingeți pentru a încărca"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Se pare că ceva nu a mers bine. Vă rugăm să încercați din nou după ceva timp. Dacă eroarea persistă, vă rugăm să contactați echipa noastră de asistență."), "terminate": MessageLookupByLibrary.simpleMessage("Terminare"), @@ -1806,7 +1819,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Aceste articole vor fi șterse din dispozitivul dvs."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Acestea vor fi șterse din toate albumele."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1822,7 +1835,7 @@ class MessageLookup extends MessageLookupByLibrary { "Această adresă de e-mail este deja folosită"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Această imagine nu are date exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Acesta este ID-ul dvs. de verificare"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1847,11 +1860,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensiune totală"), "trash": MessageLookupByLibrary.simpleMessage("Coș de gunoi"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Decupare"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacte de încredere"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Încercați din nou"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activați copia de rezervă pentru a încărca automat fișierele adăugate la acest dosar de pe dispozitiv în Ente."), @@ -1870,7 +1883,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autentificarea cu doi factori a fost resetată cu succes"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configurare doi factori"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Dezarhivare"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Dezarhivare album"), @@ -1896,10 +1909,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Se actualizează selecția dosarelor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Îmbunătățire"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Se încarcă fișiere în album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Se salvează o amintire..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1917,7 +1930,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Folosiți fotografia selectată"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spațiu utilizat"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificare eșuată, încercați din nou"), @@ -1926,7 +1939,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificare e-mail"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificați cheia de acces"), @@ -1964,7 +1977,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nu se acceptă editarea fotografiilor sau albumelor pe care nu le dețineți încă"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Slabă"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bine ați revenit!"), @@ -1973,7 +1986,7 @@ class MessageLookup extends MessageLookupByLibrary { "Contactul de încredere vă poate ajuta la recuperarea datelor."), "yearShort": MessageLookupByLibrary.simpleMessage("an"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Da"), "yesCancel": MessageLookupByLibrary.simpleMessage("Da, anulează"), "yesConvertToViewer": @@ -2005,7 +2018,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nu poți distribui cu tine însuți"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Nu aveți articole arhivate."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Contul dvs. a fost șters"), "yourMap": MessageLookupByLibrary.simpleMessage("Harta dvs."), diff --git a/mobile/lib/generated/intl/messages_ru.dart b/mobile/lib/generated/intl/messages_ru.dart index d39fd3ce07..054dafa206 100644 --- a/mobile/lib/generated/intl/messages_ru.dart +++ b/mobile/lib/generated/intl/messages_ru.dart @@ -41,7 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(name) => "Любуясь ${name}"; static String m8(count) => - "${Intl.plural(count, zero: 'Нет участников', one: '${count} участник', few: '${count} участника', other: '${count} участников')}"; + "${Intl.plural(count, zero: 'Нет участников', one: '${count} участник', other: '${count} участников')}"; static String m9(versionValue) => "Версия: ${versionValue}"; @@ -67,7 +67,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m15(albumName) => "Совместная ссылка создана для ${albumName}"; static String m16(count) => - "${Intl.plural(count, zero: 'Добавлено 0 соавторов', one: 'Добавлен 1 соавтор', few: 'Добавлено ${count} соавтора', other: 'Добавлено ${count} соавторов')}"; + "${Intl.plural(count, zero: 'Добавлено 0 соавторов', one: 'Добавлен 1 соавтор', other: 'Добавлено ${count} соавторов')}"; static String m17(email, numOfDays) => "Вы собираетесь добавить ${email} в качестве доверенного контакта. Доверенный контакт сможет восстановить ваш аккаунт, если вы будете отсутствовать ${numOfDays} дней."; @@ -81,7 +81,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Подключено к ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Удалить ${count} элемент', few: 'Удалить ${count} элемента', other: 'Удалить ${count} элементов')}"; + "${Intl.plural(count, one: 'Удалить ${count} элемент', other: 'Удалить ${count} элементов')}"; static String m22(currentlyDeleting, totalCount) => "Удаление ${currentlyDeleting} / ${totalCount}"; @@ -93,228 +93,228 @@ class MessageLookup extends MessageLookupByLibrary { "Пожалуйста, отправьте письмо на ${supportEmail} с вашего зарегистрированного адреса электронной почты"; static String m25(count, storageSaved) => - "Вы удалили ${Intl.plural(count, one: '${count} дубликат', few: '${count} дубликата', other: '${count} дубликатов')}, освободив (${storageSaved}!)"; + "Вы удалили ${Intl.plural(count, one: '${count} дубликат', other: '${count} дубликатов')}, освободив (${storageSaved}!)"; static String m26(count, formattedSize) => "${count} файлов, по ${formattedSize} каждый"; - static String m27(newEmail) => "Электронная почта изменена на ${newEmail}"; + static String m28(newEmail) => "Электронная почта изменена на ${newEmail}"; - static String m28(email) => "${email} не имеет аккаунта Ente."; + static String m29(email) => "${email} не имеет аккаунта Ente."; - static String m29(email) => + static String m30(email) => "У ${email} нет аккаунта Ente.\n\nОтправьте ему приглашение для обмена фото."; - static String m30(name) => "Обнимая ${name}"; + static String m31(name) => "Обнимая ${name}"; - static String m31(text) => "Дополнительные фото найдены для ${text}"; + static String m32(text) => "Дополнительные фото найдены для ${text}"; - static String m32(name) => "Пир с ${name}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '${formattedNumber} файл на этом устройстве был успешно сохранён', few: '${formattedNumber} файла на этом устройстве были успешно сохранены', other: '${formattedNumber} файлов на этом устройстве были успешно сохранены')}"; + static String m33(name) => "Пир с ${name}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: '${formattedNumber} файл в этом альбоме был успешно сохранён', few: '${formattedNumber} файла в этом альбоме были успешно сохранены', other: '${formattedNumber} файлов в этом альбоме были успешно сохранены')}"; + "${Intl.plural(count, one: '${formattedNumber} файл на этом устройстве был успешно сохранён', other: '${formattedNumber} файлов на этом устройстве были успешно сохранены')}"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: '${formattedNumber} файл в этом альбоме был успешно сохранён', other: '${formattedNumber} файлов в этом альбоме были успешно сохранены')}"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} ГБ каждый раз, когда кто-то подписывается на платный тариф и применяет ваш код"; - static String m36(endDate) => + static String m37(endDate) => "Бесплатный пробный период действителен до ${endDate}"; - static String m37(count) => + static String m38(count) => "Вы всё ещё сможете получить доступ к ${Intl.plural(count, one: 'нему', other: 'ним')} в Ente, пока у вас активна подписка"; - static String m38(sizeInMBorGB) => "Освободить ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Освободить ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: 'Его можно удалить с устройства, чтобы освободить ${formattedSize}', other: 'Их можно удалить с устройства, чтобы освободить ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Обработка ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "Поход с ${name}"; + static String m42(name) => "Поход с ${name}"; - static String m42(count) => - "${Intl.plural(count, one: '${count} элемент', few: '${count} элемента', other: '${count} элементов')}"; + static String m43(count) => + "${Intl.plural(count, one: '${count} элемент', other: '${count} элементов')}"; - static String m43(name) => "В последний раз с ${name}"; + static String m44(name) => "В последний раз с ${name}"; - static String m44(email) => + static String m45(email) => "${email} пригласил вас стать доверенным контактом"; - static String m45(expiryTime) => "Ссылка истечёт ${expiryTime}"; + static String m46(expiryTime) => "Ссылка истечёт ${expiryTime}"; - static String m46(email) => "Связать человека с ${email}"; + static String m47(email) => "Связать человека с ${email}"; - static String m47(personName, email) => "Это свяжет ${personName} с ${email}"; + static String m48(personName, email) => "Это свяжет ${personName} с ${email}"; - static String m48(count, formattedCount) => - "${Intl.plural(count, zero: 'нет воспоминаний', one: '${formattedCount} воспоминание', few: '${formattedCount} воспоминания', other: '${formattedCount} воспоминаний')}"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'нет воспоминаний', one: '${formattedCount} воспоминание', other: '${formattedCount} воспоминаний')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: 'Переместить элемент', other: 'Переместить элементы')}"; - static String m50(albumName) => "Успешно перемещено в ${albumName}"; + static String m51(albumName) => "Успешно перемещено в ${albumName}"; - static String m51(personName) => "Нет предложений для ${personName}"; + static String m52(personName) => "Нет предложений для ${personName}"; - static String m52(name) => "Не ${name}?"; + static String m53(name) => "Не ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Пожалуйста, свяжитесь с ${familyAdminEmail} для изменения кода."; - static String m54(name) => "Вечеринка с ${name}"; + static String m55(name) => "Вечеринка с ${name}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надёжность пароля: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Пожалуйста, обратитесь в поддержку ${providerName}, если с вас сняли деньги"; - static String m57(name, age) => "${name} исполнилось ${age}!"; + static String m58(name, age) => "${name} исполнилось ${age}!"; - static String m58(name, age) => "${name} скоро исполнится ${age}"; - - static String m59(count) => - "${Intl.plural(count, zero: 'Нет фото', one: '1 фото', other: '${count} фото')}"; + static String m59(name, age) => "${name} скоро исполнится ${age}"; static String m60(count) => - "${Intl.plural(count, zero: '0 фотографий', one: '1 фотография', few: '${count} фотографии', other: '${count} фотографий')}"; + "${Intl.plural(count, zero: 'Нет фото', one: '1 фото', other: '${count} фото')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 фотографий', one: '1 фотография', other: '${count} фотографий')}"; + + static String m62(endDate) => "Бесплатный пробный период действителен до ${endDate}.\nПосле этого вы можете выбрать платный тариф."; - static String m62(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; + static String m63(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; - static String m63(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; + static String m64(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; - static String m64(name) => "Позируя с ${name}"; + static String m65(name) => "Позируя с ${name}"; - static String m65(folderName) => "Обработка ${folderName}..."; + static String m66(folderName) => "Обработка ${folderName}..."; - static String m66(storeName) => "Оцените нас в ${storeName}"; + static String m67(storeName) => "Оцените нас в ${storeName}"; - static String m67(name) => "Вы переназначены на ${name}"; + static String m68(name) => "Вы переназначены на ${name}"; - static String m68(days, email) => + static String m69(days, email) => "Вы сможете получить доступ к аккаунту через ${days} дней. Уведомление будет отправлено на ${email}."; - static String m69(email) => + static String m70(email) => "Теперь вы можете восстановить аккаунт ${email}, установив новый пароль."; - static String m70(email) => "${email} пытается восстановить ваш аккаунт."; + static String m71(email) => "${email} пытается восстановить ваш аккаунт."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Вы оба получаете ${storageInGB} ГБ* бесплатно"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} будет удалён из этого общего альбома\n\nВсе фото, добавленные этим пользователем, также будут удалены из альбома"; - static String m73(endDate) => "Подписка будет продлена ${endDate}"; + static String m74(endDate) => "Подписка будет продлена ${endDate}"; - static String m74(name) => "Путешествие с ${name}"; + static String m75(name) => "Путешествие с ${name}"; - static String m113(count) => - "${Intl.plural(count, one: '${count} результат найден', few: '${count} результата найдено', other: '${count} результатов найдено')}"; + static String m76(count) => + "${Intl.plural(count, one: '${count} результат найден', other: '${count} результатов найдено')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Несоответствие длины разделов: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} выбрано"; + static String m78(count) => "${count} выбрано"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} выбрано (${yourCount} ваших)"; - static String m78(name) => "Селфи с ${name}"; + static String m80(name) => "Селфи с ${name}"; - static String m79(verificationID) => + static String m81(verificationID) => "Вот мой идентификатор подтверждения: ${verificationID} для ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Привет, можешь подтвердить, что это твой идентификатор подтверждения ente.io: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Реферальный код Ente: ${referralCode} \n\nПримените его в разделе «Настройки» → «Общие» → «Рефералы», чтобы получить ${referralStorageInGB} ГБ бесплатно после подписки на платный тариф\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поделиться с конкретными людьми', one: 'Доступно 1 человеку', other: 'Доступно ${numberOfPeople} людям')}"; - static String m83(emailIDs) => "Доступен для ${emailIDs}"; + static String m85(emailIDs) => "Доступен для ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Это ${fileType} будет удалено с вашего устройства."; - static String m85(fileType) => + static String m87(fileType) => "Это ${fileType} есть и в Ente, и на вашем устройстве."; - static String m86(fileType) => "Это ${fileType} будет удалено из Ente."; + static String m88(fileType) => "Это ${fileType} будет удалено из Ente."; - static String m87(name) => "Спорт с ${name}"; + static String m89(name) => "Спорт с ${name}"; - static String m88(name) => "В центре внимания ${name}"; + static String m90(name) => "В центре внимания ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} ГБ"; + static String m91(storageAmountInGB) => "${storageAmountInGB} ГБ"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "Использовано ${usedAmount} ${usedStorageUnit} из ${totalAmount} ${totalStorageUnit}"; - static String m91(id) => + static String m93(id) => "Ваш ${id} уже связан с другим аккаунтом Ente.\nЕсли вы хотите использовать ${id} с этим аккаунтом, пожалуйста, свяжитесь с нашей службой поддержки"; - static String m92(endDate) => "Ваша подписка будет отменена ${endDate}"; + static String m94(endDate) => "Ваша подписка будет отменена ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} воспоминаний сохранено"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Нажмите для загрузки. Загрузка игнорируется из-за ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Они тоже получат ${storageAmountInGB} ГБ"; - static String m96(email) => "Это идентификатор подтверждения ${email}"; - - static String m97(count) => - "${Intl.plural(count, one: 'Эта неделя, ${count} год назад', few: 'Эта неделя, ${count} года назад', other: 'Эта неделя, ${count} лет назад')}"; - - static String m98(dateFormat) => "${dateFormat} сквозь годы"; + static String m98(email) => "Это идентификатор подтверждения ${email}"; static String m99(count) => - "${Intl.plural(count, zero: 'Скоро', one: '1 день', few: '${count} дня', other: '${count} дней')}"; + "${Intl.plural(count, one: 'Эта неделя, ${count} год назад', other: 'Эта неделя, ${count} лет назад')}"; - static String m100(year) => "Поездка в ${year}"; + static String m100(dateFormat) => "${dateFormat} сквозь годы"; - static String m101(location) => "Поездка в ${location}"; + static String m101(count) => + "${Intl.plural(count, zero: 'Скоро', one: '1 день', other: '${count} дней')}"; - static String m102(email) => + static String m102(year) => "Поездка в ${year}"; + + static String m103(location) => "Поездка в ${location}"; + + static String m104(email) => "Вы приглашены стать доверенным контактом ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Тип галереи ${galleryType} не поддерживает переименование"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Загрузка игнорируется из-за ${ignoreReason}"; - static String m105(count) => "Сохранение ${count} воспоминаний..."; + static String m107(count) => "Сохранение ${count} воспоминаний..."; - static String m106(endDate) => "Действительно до ${endDate}"; + static String m108(endDate) => "Действительно до ${endDate}"; - static String m107(email) => "Подтвердить ${email}"; + static String m109(email) => "Подтвердить ${email}"; - static String m108(count) => + static String m111(count) => "${Intl.plural(count, zero: 'Добавлено 0 зрителей', one: 'Добавлен 1 зритель', other: 'Добавлено ${count} зрителей')}"; - static String m109(email) => "Мы отправили письмо на ${email}"; + static String m112(email) => "Мы отправили письмо на ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} год назад', few: '${count} года назад', other: '${count} лет назад')}"; - static String m111(name) => "Вы и ${name}"; + static String m114(name) => "Вы и ${name}"; - static String m112(storageSaved) => "Вы успешно освободили ${storageSaved}!"; + static String m115(storageSaved) => "Вы успешно освободили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -540,26 +540,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Резервное копирование видео"), "beach": MessageLookupByLibrary.simpleMessage("Песок и море"), "birthday": MessageLookupByLibrary.simpleMessage("День рождения"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Распродажа в \"Черную пятницу\""), "blog": MessageLookupByLibrary.simpleMessage("Блог"), - "cLBulkEdit": - MessageLookupByLibrary.simpleMessage("Массовое редактирование дат"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "Теперь вы можете выбрать несколько фото и отредактировать дату/время быстро и сразу для всех. Также поддерживается смещение дат."), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage( - "Ограничения семейного тарифа"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Теперь вы можете установить ограничения на объём хранилища, которое могут использовать члены вашей семьи."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Новая иконка"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "Наконец-то новая иконка приложения, которая, как мы считаем, лучше всего отражает нашу работу. Мы также добавили переключатель иконок, чтобы вы могли продолжать использовать старую иконку."), - "cLMemories": MessageLookupByLibrary.simpleMessage("Воспоминания"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "Откройте заново свои особенные моменты — в центре внимания ваши любимые люди, поездки и праздники, лучшие снимки и многое другое. Для наилучших впечатлений включите машинное обучение и отметьте себя и своих друзей."), - "cLWidgets": MessageLookupByLibrary.simpleMessage("Виджеты"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "Теперь доступны виджеты домашнего экрана, интегрированные с воспоминаниями. Они покажут ваши особенные моменты, не открывая приложения."), "cachedData": MessageLookupByLibrary.simpleMessage("Кэшированные данные"), "calculating": MessageLookupByLibrary.simpleMessage("Подсчёт..."), @@ -632,6 +616,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Нажмите"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Нажмите на меню дополнительных действий"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Закрыть"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Группировать по времени съёмки"), @@ -731,6 +717,8 @@ class MessageLookup extends MessageLookupByLibrary { "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage( "Доступно критическое обновление"), "crop": MessageLookupByLibrary.simpleMessage("Обрезать"), + "curatedMemories": + MessageLookupByLibrary.simpleMessage("Отобранные воспоминания"), "currentUsageIs": MessageLookupByLibrary.simpleMessage( "Текущее использование составляет "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("выполняется"), @@ -879,16 +867,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Электронная почта"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Электронная почта уже зарегистрирована."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Электронная почта не зарегистрирована."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Подтверждение входа по почте"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Отправить логи по электронной почте"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("Экстренные контакты"), "empty": MessageLookupByLibrary.simpleMessage("Очистить"), @@ -966,7 +954,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Экспортировать ваши данные"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Найдены дополнительные фото"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Лицо ещё не кластеризовано. Пожалуйста, попробуйте позже"), "faceRecognition": @@ -1004,7 +992,7 @@ class MessageLookup extends MessageLookupByLibrary { "faqs": MessageLookupByLibrary.simpleMessage("Часто задаваемые вопросы"), "favorite": MessageLookupByLibrary.simpleMessage("В избранное"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Обратная связь"), "file": MessageLookupByLibrary.simpleMessage("Файл"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -1018,8 +1006,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типы файлов"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типы и названия файлов"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файлы удалены"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файлы сохранены в галерею"), @@ -1035,27 +1023,27 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Найденные лица"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Полученное бесплатное хранилище"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Доступное бесплатное хранилище"), "freeTrial": MessageLookupByLibrary.simpleMessage("Бесплатный пробный период"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Освободить место на устройстве"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Освободите место на устройстве, удалив файлы, которые уже сохранены в резервной копии."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Освободить место"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "В галерее отображается до 1000 воспоминаний"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Генерация ключей шифрования..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти в настройки"), "googlePlayId": @@ -1070,6 +1058,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Гостевой просмотр"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Для включения гостевого просмотра, пожалуйста, настройте код или блокировку экрана в настройках устройства."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Мы не отслеживаем установки приложений. Нам поможет, если скажете, как вы нас нашли!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1086,7 +1076,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Скрыть общие элементы из основной галереи"), "hiding": MessageLookupByLibrary.simpleMessage("Скрытие..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Размещено на OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Как это работает"), @@ -1144,7 +1134,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка сохраняется, обратитесь в нашу службу поддержки."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "На элементах отображается количество дней, оставшихся до их безвозвратного удаления"), @@ -1166,7 +1156,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Пожалуйста, помогите нам с этой информацией"), "language": MessageLookupByLibrary.simpleMessage("Язык"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("Последнее обновление"), "lastYearsTrip": @@ -1180,7 +1170,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Наследие"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Наследуемые аккаунты"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Наследие позволяет доверенным контактам получить доступ к вашему аккаунту в ваше отсутствие."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1198,7 +1188,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("чтобы быстрее делиться"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Включена"), "linkExpired": MessageLookupByLibrary.simpleMessage("Истекла"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Срок действия ссылки"), "linkHasExpired": @@ -1207,13 +1197,11 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Связать человека"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("чтобы было удобнее делиться"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Живые фото"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Вы можете поделиться подпиской с вашей семьёй"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Мы сохранили уже более 30 миллионов воспоминаний"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Мы храним 3 копии ваших данных, одну из них — в бункере"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1271,6 +1259,8 @@ class MessageLookup extends MessageLookupByLibrary { "Нажмите с удержанием на электронную почту для подтверждения сквозного шифрования."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Нажмите с удержанием на элемент для просмотра в полноэкранном режиме"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Видео не зациклено"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Видео зациклено"), @@ -1299,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Я"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Мерч"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Объединить с существующим"), @@ -1332,14 +1322,14 @@ class MessageLookup extends MessageLookupByLibrary { "mostRelevant": MessageLookupByLibrary.simpleMessage("Самые актуальные"), "mountains": MessageLookupByLibrary.simpleMessage("За холмами"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Переместите выбранные фото на одну дату"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Переместить в альбом"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Переместить в скрытый альбом"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Перемещено в корзину"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1356,6 +1346,7 @@ class MessageLookup extends MessageLookupByLibrary { "newLocation": MessageLookupByLibrary.simpleMessage("Новое местоположение"), "newPerson": MessageLookupByLibrary.simpleMessage("Новый человек"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Новый диапазон"), "newToEnte": MessageLookupByLibrary.simpleMessage("Впервые в Ente"), "newest": MessageLookupByLibrary.simpleMessage("Недавние"), @@ -1394,10 +1385,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Нет результатов"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Нет результатов"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Системная блокировка не найдена"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("Не этот человек?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( @@ -1410,7 +1401,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage("В ente"), "onTheRoad": MessageLookupByLibrary.simpleMessage("Снова в пути"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Только он(а)"), "oops": MessageLookupByLibrary.simpleMessage("Ой"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1441,7 +1435,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Подключение завершено"), "panorama": MessageLookupByLibrary.simpleMessage("Панорама"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Проверка всё ещё ожидается"), "passkey": MessageLookupByLibrary.simpleMessage("Ключ доступа"), @@ -1451,7 +1445,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Пароль успешно изменён"), "passwordLock": MessageLookupByLibrary.simpleMessage("Защита паролем"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Надёжность пароля определяется его длиной, используемыми символами и присутствием среди 10000 самых популярных паролей"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1462,7 +1456,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Платёж не удался"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "К сожалению, ваш платёж не удался. Пожалуйста, свяжитесь с поддержкой, и мы вам поможем!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Элементы в очереди"), "pendingSync": @@ -1476,21 +1470,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Удалить безвозвратно"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Удалить с устройства безвозвратно?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Имя человека"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("Пушистые спутники"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Описания фото"), "photoGridSize": MessageLookupByLibrary.simpleMessage("Размер сетки фото"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("фото"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Фото"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Добавленные вами фото будут удалены из альбома"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Фото сохранят относительную разницу во времени"), @@ -1502,7 +1496,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Воспроизвести альбом на ТВ"), "playOriginal": MessageLookupByLibrary.simpleMessage("Воспроизвести оригинал"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Воспроизвести поток"), "playstoreSubscription": @@ -1516,14 +1510,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Пожалуйста, обратитесь в поддержку, если проблема сохраняется"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Пожалуйста, предоставьте разрешения"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Пожалуйста, войдите снова"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Пожалуйста, выберите быстрые ссылки для удаления"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Пожалуйста, попробуйте снова"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1538,7 +1532,7 @@ class MessageLookup extends MessageLookupByLibrary { "Пожалуйста, подождите некоторое время перед повторной попыткой"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Пожалуйста, подождите, это займёт некоторое время."), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("Подготовка логов..."), "preserveMore": @@ -1558,7 +1552,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Продолжить"), "processed": MessageLookupByLibrary.simpleMessage("Обработано"), "processing": MessageLookupByLibrary.simpleMessage("Обработка"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Обработка видео"), "publicLinkCreated": @@ -1572,12 +1566,14 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оценить приложение"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцените нас"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("Переназначить \"Меня\""), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Переназначение..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Восстановить"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Восстановить аккаунт"), @@ -1586,7 +1582,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Восстановить аккаунт"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Восстановление начато"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Ключ восстановления"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1601,12 +1597,12 @@ class MessageLookup extends MessageLookupByLibrary { "Ключ восстановления подтверждён"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Ваш ключ восстановления — единственный способ восстановить ваши фото, если вы забудете пароль. Вы можете найти ключ восстановления в разделе «Настройки» → «Аккаунт».\n\nПожалуйста, введите ваш ключ восстановления здесь, чтобы убедиться, что вы сохранили его правильно."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Успешное восстановление!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Доверенный контакт пытается получить доступ к вашему аккаунту"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Текущее устройство недостаточно мощное для проверки вашего пароля, но мы можем сгенерировать его снова так, чтобы он работал на всех устройствах.\n\nПожалуйста, войдите, используя ваш ключ восстановления, и сгенерируйте пароль (при желании вы можете использовать тот же самый)."), "recreatePasswordTitle": @@ -1622,7 +1618,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Даёте этот код своим друзьям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Они подписываются на платный тариф"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Рефералы"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Реферальная программа временно приостановлена"), @@ -1654,7 +1650,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Удалить ссылку"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Удалить участника"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Удалить метку человека"), "removePublicLink": @@ -1676,7 +1672,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Переименовать файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Продлить подписку"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), "reportBug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), @@ -1703,7 +1699,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Посмотреть предложения"), "right": MessageLookupByLibrary.simpleMessage("Вправо"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Повернуть"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Повернуть влево"), "rotateRight": MessageLookupByLibrary.simpleMessage("Повернуть вправо"), @@ -1760,8 +1756,8 @@ class MessageLookup extends MessageLookupByLibrary { "Приглашайте людей, и здесь появятся все фото, которыми они поделились"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Люди появятся здесь после завершения обработки и синхронизации"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Безопасность"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Просматривать публичные ссылки на альбомы в приложении"), @@ -1779,7 +1775,7 @@ class MessageLookup extends MessageLookupByLibrary { "Выберите папки для резервного копирования"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage( "Выберите элементы для добавления"), - "selectLanguage": MessageLookupByLibrary.simpleMessage("Выбрать язык"), + "selectLanguage": MessageLookupByLibrary.simpleMessage("Выберите язык"), "selectMailApp": MessageLookupByLibrary.simpleMessage( "Выберите почтовое приложение"), "selectMorePhotos": @@ -1810,9 +1806,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage( "Выбранные элементы будут отвязаны от этого человека, но не удалены из вашей библиотеки."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Отправить"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Отправить электронное письмо"), @@ -1831,7 +1827,7 @@ class MessageLookup extends MessageLookupByLibrary { "setCover": MessageLookupByLibrary.simpleMessage("Установить обложку"), "setLabel": MessageLookupByLibrary.simpleMessage("Установить"), "setNewPassword": - MessageLookupByLibrary.simpleMessage("Установить новый пароль"), + MessageLookupByLibrary.simpleMessage("Установите новый пароль"), "setNewPin": MessageLookupByLibrary.simpleMessage("Установите новый PIN-код"), "setPasswordTitle": @@ -1847,16 +1843,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Поделиться альбомом"), "shareLink": MessageLookupByLibrary.simpleMessage("Поделиться ссылкой"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Делитесь только с теми, с кем хотите"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Скачай Ente, чтобы мы могли легко делиться фото и видео в оригинальном качестве\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поделиться с пользователями, не использующими Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Поделитесь своим первым альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1867,7 +1863,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Новые общие фото"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Получать уведомления, когда кто-то добавляет фото в общий альбом, в котором вы состоите"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Со мной поделились"), "sharedWithYou": @@ -1876,7 +1872,7 @@ class MessageLookup extends MessageLookupByLibrary { "shiftDatesAndTime": MessageLookupByLibrary.simpleMessage("Сместить даты и время"), "showMemories": - MessageLookupByLibrary.simpleMessage("Показать воспоминания"), + MessageLookupByLibrary.simpleMessage("Показывать воспоминания"), "showPerson": MessageLookupByLibrary.simpleMessage("Показать человека"), "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage("Выйти с других устройств"), @@ -1886,11 +1882,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Выйти с других устройств"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я согласен с условиями предоставления услуг и политикой конфиденциальности"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Оно будет удалено из всех альбомов."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), "social": MessageLookupByLibrary.simpleMessage("Социальные сети"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1918,6 +1914,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "К сожалению, мы не смогли сгенерировать безопасные ключи на этом устройстве.\n\nПожалуйста, зарегистрируйтесь с другого устройства."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Сортировать"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Сортировать по"), "sortNewestFirst": @@ -1925,8 +1923,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Сначала старые"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Успех"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("Вы в центре внимания"), "startAccountRecoveryTitle": @@ -1941,15 +1939,15 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Хранилище"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Семья"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Вы"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Превышен лимит хранилища"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("Информация о потоке"), "strongStrength": MessageLookupByLibrary.simpleMessage("Высокая"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Подписаться"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Вам нужна активная платная подписка, чтобы включить общий доступ."), @@ -1967,7 +1965,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Предложить идею"), "sunrise": MessageLookupByLibrary.simpleMessage("На горизонте"), "support": MessageLookupByLibrary.simpleMessage("Поддержка"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронизация остановлена"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронизация..."), @@ -1980,7 +1978,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Нажмите для разблокировки"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Нажмите для загрузки"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка сохраняется, обратитесь в нашу службу поддержки."), "terminate": MessageLookupByLibrary.simpleMessage("Завершить"), @@ -2004,7 +2002,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Эти элементы будут удалены с вашего устройства."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Они будут удалены из всех альбомов."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -2021,12 +2019,12 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Это фото не имеет данных EXIF"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Это я!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Это ваш идентификатор подтверждения"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("Эта неделя сквозь годы"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Это завершит ваш сеанс на следующем устройстве:"), @@ -2038,7 +2036,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Это удалит публичные ссылки всех выбранных быстрых ссылок."), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Для блокировки приложения, пожалуйста, настройте код или экран блокировки в настройках устройства."), @@ -2052,20 +2050,21 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("всего"), "totalSize": MessageLookupByLibrary.simpleMessage("Общий размер"), "trash": MessageLookupByLibrary.simpleMessage("Корзина"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Сократить"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Доверенные контакты"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Попробовать снова"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Включите резервное копирование, чтобы автоматически загружать файлы из этой папки на устройстве в Ente."), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 месяца в подарок на годовом тарифе"), - "twofactor": MessageLookupByLibrary.simpleMessage("Двухфакторная"), + "twofactor": MessageLookupByLibrary.simpleMessage( + "Двухфакторная аутентификация"), "twofactorAuthenticationHasBeenDisabled": MessageLookupByLibrary.simpleMessage( "Двухфакторная аутентификация отключена"), @@ -2077,7 +2076,7 @@ class MessageLookup extends MessageLookupByLibrary { "Двухфакторная аутентификация успешно сброшена"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Настройка двухфакторной аутентификации"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Извлечь из архива"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Извлечь альбом из архива"), @@ -2100,10 +2099,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Обновление выбора папок..."), "upgrade": MessageLookupByLibrary.simpleMessage("Улучшить"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Загрузка файлов в альбом..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Сохранение 1 воспоминания..."), "upto50OffUntil4thDec": @@ -2122,7 +2121,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Использовать выбранное фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Использовано места"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Проверка не удалась, пожалуйста, попробуйте снова"), @@ -2131,7 +2130,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Подтвердить"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Подтвердить электронную почту"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Подтвердить"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Подтвердить ключ доступа"), @@ -2142,8 +2141,6 @@ class MessageLookup extends MessageLookupByLibrary { "Проверка ключа восстановления..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Информация о видео"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("видео"), - "videoStreaming": - MessageLookupByLibrary.simpleMessage("Потоковое видео"), "videos": MessageLookupByLibrary.simpleMessage("Видео"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Просмотр активных сессий"), @@ -2159,7 +2156,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Увидеть ключ восстановления"), "viewer": MessageLookupByLibrary.simpleMessage("Зритель"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Пожалуйста, посетите web.ente.io для управления вашей подпиской"), "waitingForVerification": @@ -2172,7 +2169,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Мы не поддерживаем редактирование фото и альбомов, которые вам пока не принадлежат"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Низкая"), "welcomeBack": MessageLookupByLibrary.simpleMessage("С возвращением!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Что нового"), @@ -2180,7 +2177,7 @@ class MessageLookup extends MessageLookupByLibrary { "Доверенный контакт может помочь в восстановлении ваших данных."), "yearShort": MessageLookupByLibrary.simpleMessage("год"), "yearly": MessageLookupByLibrary.simpleMessage("Ежегодно"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Да"), "yesCancel": MessageLookupByLibrary.simpleMessage("Да, отменить"), "yesConvertToViewer": @@ -2194,7 +2191,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage( "Да, сбросить данные человека"), "you": MessageLookupByLibrary.simpleMessage("Вы"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Вы на семейном тарифе!"), "youAreOnTheLatestVersion": MessageLookupByLibrary.simpleMessage( @@ -2213,7 +2210,7 @@ class MessageLookup extends MessageLookupByLibrary { "Вы не можете поделиться с самим собой"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас нет архивных элементов."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Ваш аккаунт был удалён"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша карта"), diff --git a/mobile/lib/generated/intl/messages_sl.dart b/mobile/lib/generated/intl/messages_sl.dart index d41d848b0f..a0e7022e1c 100644 --- a/mobile/lib/generated/intl/messages_sl.dart +++ b/mobile/lib/generated/intl/messages_sl.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'sl'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_sr.dart b/mobile/lib/generated/intl/messages_sr.dart new file mode 100644 index 0000000000..3ec37429d1 --- /dev/null +++ b/mobile/lib/generated/intl/messages_sr.dart @@ -0,0 +1,31 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a sr locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'sr'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person.") + }; +} diff --git a/mobile/lib/generated/intl/messages_sv.dart b/mobile/lib/generated/intl/messages_sv.dart index 372c414f64..0f01aeda2f 100644 --- a/mobile/lib/generated/intl/messages_sv.dart +++ b/mobile/lib/generated/intl/messages_sv.dart @@ -47,62 +47,62 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} filer, ${formattedSize} vardera"; - static String m29(email) => + static String m30(email) => "${email} har inte ett Ente-konto.\n\nSkicka dem en inbjudan för att dela bilder."; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB varje gång någon registrerar sig för en betalplan och tillämpar din kod"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} objekt', other: '${count} objekt')}"; - static String m45(expiryTime) => "Länken upphör att gälla ${expiryTime}"; + static String m46(expiryTime) => "Länken upphör att gälla ${expiryTime}"; - static String m52(name) => "Inte ${name}?"; + static String m53(name) => "Inte ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Kontakta ${familyAdminEmail} för att ändra din kod."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Lösenordsstyrka: ${passwordStrengthValue}"; - static String m66(storeName) => "Betygsätt oss på ${storeName}"; + static String m67(storeName) => "Betygsätt oss på ${storeName}"; - static String m71(storageInGB) => "3. Ni får båda ${storageInGB} GB* gratis"; + static String m72(storageInGB) => "3. Ni får båda ${storageInGB} GB* gratis"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} kommer att tas bort från detta delade album\n\nAlla bilder som lagts till av dem kommer också att tas bort från albumet"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} resultat hittades', other: '${count} resultat hittades')}"; - static String m79(verificationID) => + static String m81(verificationID) => "Här är mitt verifierings-ID: ${verificationID} för ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Hallå, kan du bekräfta att detta är ditt ente.io verifierings-ID: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente värvningskod: ${referralCode} \n\nTillämpa den i Inställningar → Allmänt → Hänvisningar för att få ${referralStorageInGB} GB gratis när du registrerar dig för en betalplan\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Dela med specifika personer', one: 'Delad med en person', other: 'Delad med ${numberOfPeople} personer')}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "De får också ${storageAmountInGB} GB"; - static String m96(email) => "Detta är ${email}s verifierings-ID"; + static String m98(email) => "Detta är ${email}s verifierings-ID"; - static String m105(count) => "Bevarar ${count} minnen..."; + static String m107(count) => "Bevarar ${count} minnen..."; - static String m107(email) => "Bekräfta ${email}"; + static String m109(email) => "Bekräfta ${email}"; - static String m109(email) => + static String m112(email) => "Vi har skickat ett e-postmeddelande till ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} år sedan', other: '${count} år sedan')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -164,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { "Säkerhetskopieringsinställningar"), "backupStatus": MessageLookupByLibrary.simpleMessage("Säkerhetskopieringsstatus"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blog": MessageLookupByLibrary.simpleMessage("Blogg"), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( "Tyvärr kan detta album inte öppnas i appen."), @@ -192,6 +193,8 @@ class MessageLookup extends MessageLookupByLibrary { "claimed": MessageLookupByLibrary.simpleMessage("Nyttjad"), "claimedStorageSoFar": m14, "clearIndexes": MessageLookupByLibrary.simpleMessage("Rensa index"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Stäng"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Kod tillämpad"), @@ -292,7 +295,7 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-post"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-postadress redan registrerad."), - "emailNoEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-postadressen är inte registrerad."), "encryption": MessageLookupByLibrary.simpleMessage("Kryptering"), @@ -342,7 +345,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Glömt lösenord"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis lagring begärd"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Gratis lagringsutrymme som kan användas"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis provperiod"), @@ -351,6 +354,8 @@ class MessageLookup extends MessageLookupByLibrary { "goToSettings": MessageLookupByLibrary.simpleMessage("Gå till inställningar"), "guestView": MessageLookupByLibrary.simpleMessage("Gästvy"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "help": MessageLookupByLibrary.simpleMessage("Hjälp"), "howItWorks": MessageLookupByLibrary.simpleMessage("Så här fungerar det"), @@ -379,7 +384,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bjud in dina vänner"), "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( "Bjud in dina vänner till Ente"), - "itemCount": m42, + "itemCount": m43, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Valda objekt kommer att tas bort från detta album"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Behåll foton"), @@ -392,7 +397,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Enhetsgräns"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiverat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Upphört"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Länken upphör"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Länk har upphört att gälla"), @@ -404,6 +409,8 @@ class MessageLookup extends MessageLookupByLibrary { "loginTerms": MessageLookupByLibrary.simpleMessage( "Genom att klicka på logga in godkänner jag användarvillkoren och våran integritetspolicy"), "logout": MessageLookupByLibrary.simpleMessage("Logga ut"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "lostDevice": MessageLookupByLibrary.simpleMessage("Förlorad enhet?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Maskininlärning"), @@ -427,6 +434,7 @@ class MessageLookup extends MessageLookupByLibrary { "never": MessageLookupByLibrary.simpleMessage("Aldrig"), "newAlbum": MessageLookupByLibrary.simpleMessage("Nytt album"), "newPerson": MessageLookupByLibrary.simpleMessage("Ny person"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "next": MessageLookupByLibrary.simpleMessage("Nästa"), "no": MessageLookupByLibrary.simpleMessage("Nej"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Ingen"), @@ -440,9 +448,12 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Inga resultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Inga resultat hittades"), - "notPersonLabel": m52, + "notPersonLabel": m53, "ok": MessageLookupByLibrary.simpleMessage("OK"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "oops": MessageLookupByLibrary.simpleMessage("Hoppsan"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Oj, något gick fel"), @@ -453,7 +464,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Lösenordet har ändrats"), "passwordLock": MessageLookupByLibrary.simpleMessage("Lösenordskydd"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Vi lagrar inte detta lösenord, så om du glömmer bort det, kan vi inte dekryptera dina data"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -470,7 +481,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Integritetspolicy"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Offentlig länk aktiverad"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Återställ"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Återställ konto"), @@ -500,7 +513,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Ge denna kod till dina vänner"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. De registrerar sig för en betalplan"), - "referralStep3": m71, + "referralStep3": m72, "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Hänvisningar är för närvarande pausade"), "remove": MessageLookupByLibrary.simpleMessage("Ta bort"), @@ -511,7 +524,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Radera länk"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Ta bort användaren"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "Några av de objekt som du tar bort lades av andra personer, och du kommer att förlora tillgång till dem"), "removeWithQuestionMark": @@ -544,7 +557,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Albumnamn"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Filtyper och namn"), - "searchResultCount": m113, + "searchResultCount": m76, "selectAlbum": MessageLookupByLibrary.simpleMessage("Välj album"), "selectAll": MessageLookupByLibrary.simpleMessage("Markera allt"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( @@ -567,14 +580,14 @@ class MessageLookup extends MessageLookupByLibrary { "share": MessageLookupByLibrary.simpleMessage("Dela"), "shareALink": MessageLookupByLibrary.simpleMessage("Dela en länk"), "shareLink": MessageLookupByLibrary.simpleMessage("Dela länk"), - "shareMyVerificationID": m79, - "shareTextConfirmOthersVerificationID": m80, + "shareMyVerificationID": m81, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Ladda ner Ente så att vi enkelt kan dela bilder och videor med originell kvalitet\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Dela med icke-Ente användare"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Dela ditt första album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -601,11 +614,13 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Tyvärr, vi kunde inte generera säkra nycklar på den här enheten.\n\nVänligen registrera dig från en annan enhet."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sortera"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sortera efter"), "status": MessageLookupByLibrary.simpleMessage("Status"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Du"), - "storageInGB": m89, + "storageInGB": m91, "strongStrength": MessageLookupByLibrary.simpleMessage("Starkt"), "subscribe": MessageLookupByLibrary.simpleMessage("Prenumerera"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( @@ -625,12 +640,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Återställningsnyckeln du angav är felaktig"), "theme": MessageLookupByLibrary.simpleMessage("Tema"), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "thisCanBeUsedToRecoverYourAccountIfYou": MessageLookupByLibrary.simpleMessage( "Detta kan användas för att återställa ditt konto om du förlorar din andra faktor"), "thisDevice": MessageLookupByLibrary.simpleMessage("Den här enheten"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Detta är ditt verifierings-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -658,7 +673,7 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Uppdaterar mappval..."), "upgrade": MessageLookupByLibrary.simpleMessage("Uppgradera"), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Bevarar 1 minne..."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( @@ -671,7 +686,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekräfta"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekräfta e-postadress"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifiera nyckel"), "verifyPassword": @@ -687,12 +702,12 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Visa återställningsnyckel"), "viewer": MessageLookupByLibrary.simpleMessage("Bildvy"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Svagt"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Välkommen tillbaka!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nyheter"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, avbryt"), "yesConvertToViewer": diff --git a/mobile/lib/generated/intl/messages_ta.dart b/mobile/lib/generated/intl/messages_ta.dart index 30c00c6d72..0c2011501d 100644 --- a/mobile/lib/generated/intl/messages_ta.dart +++ b/mobile/lib/generated/intl/messages_ta.dart @@ -26,7 +26,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("மீண்டும் வருக!"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "உங்கள் கணக்கை நீக்குவதற்கான முக்கிய காரணம் என்ன?"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "cancel": MessageLookupByLibrary.simpleMessage("ரத்து செய்"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "கணக்கு நீக்குதலை உறுதிப்படுத்தவும்"), "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( @@ -39,15 +42,35 @@ class MessageLookup extends MessageLookupByLibrary { "deleteReason1": MessageLookupByLibrary.simpleMessage( "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை"), "email": MessageLookupByLibrary.simpleMessage("மின்னஞ்சல்"), + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "மின்னஞ்சல் முன்பே பதிவுசெய்யப்பட்டுள்ளது."), + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "மின்னஞ்சல் பதிவு செய்யப்படவில்லை."), "enterValidEmail": MessageLookupByLibrary.simpleMessage( "சரியான மின்னஞ்சல் முகவரியை உள்ளிடவும்."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( "உங்கள் மின்னஞ்சல் முகவரியை உள்ளிடவும்"), "feedback": MessageLookupByLibrary.simpleMessage("பின்னூட்டம்"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("தவறான மின்னஞ்சல் முகவரி"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "இந்த தகவலுடன் தயவுசெய்து எங்களுக்கு உதவுங்கள்"), - "verify": MessageLookupByLibrary.simpleMessage("சரிபார்க்கவும்") + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "selectReason": MessageLookupByLibrary.simpleMessage( + "காரணத்தைத் தேர்ந்தெடுக்கவும்"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), + "verify": MessageLookupByLibrary.simpleMessage("சரிபார்க்கவும்"), + "yourAccountHasBeenDeleted": + MessageLookupByLibrary.simpleMessage("உங்கள் கணக்கு நீக்கப்பட்டது") }; } diff --git a/mobile/lib/generated/intl/messages_te.dart b/mobile/lib/generated/intl/messages_te.dart index 5e415c9da0..e2878dee28 100644 --- a/mobile/lib/generated/intl/messages_te.dart +++ b/mobile/lib/generated/intl/messages_te.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'te'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_th.dart b/mobile/lib/generated/intl/messages_th.dart index df68827eed..14b80844d9 100644 --- a/mobile/lib/generated/intl/messages_th.dart +++ b/mobile/lib/generated/intl/messages_th.dart @@ -31,19 +31,19 @@ class MessageLookup extends MessageLookupByLibrary { static String m24(supportEmail) => "กรุณาส่งอีเมลไปที่ ${supportEmail} จากที่อยู่อีเมลที่คุณลงทะเบียนไว้"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "กำลังประมวลผล ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} รายการ')}"; + static String m43(count) => "${Intl.plural(count, other: '${count} รายการ')}"; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "ความแข็งแรงของรหัสผ่าน: ${passwordStrengthValue}"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "ใช้ไป ${usedAmount} ${usedStorageUnit} จาก ${totalAmount} ${totalStorageUnit}"; - static String m109(email) => "เราได้ส่งจดหมายไปยัง ${email}"; + static String m112(email) => "เราได้ส่งจดหมายไปยัง ${email}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -78,6 +78,7 @@ class MessageLookup extends MessageLookupByLibrary { "เหตุผลหลักที่คุณลบบัญชีคืออะไร?"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "โปรดตรวจสอบสิทธิ์เพื่อดูคีย์การกู้คืนของคุณ"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "canOnlyCreateLinkForFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "สามารถสร้างลิงก์ได้เฉพาะไฟล์ที่คุณเป็นเจ้าของ"), @@ -87,6 +88,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("เปลี่ยนรหัสผ่าน"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "โปรดตรวจสอบกล่องจดหมาย (และสแปม) ของคุณ เพื่อยืนยันให้เสร็จสิ้น"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "คัดลอกรหัสไปยังคลิปบอร์ดแล้ว"), "collectPhotos": MessageLookupByLibrary.simpleMessage("รวบรวมรูปภาพ"), @@ -167,8 +170,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("เพิ่มคำอธิบาย..."), "forgotPassword": MessageLookupByLibrary.simpleMessage("ลืมรหัสผ่าน"), "freeTrial": MessageLookupByLibrary.simpleMessage("ทดลองใช้ฟรี"), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("ไปที่การตั้งค่า"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hide": MessageLookupByLibrary.simpleMessage("ซ่อน"), "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("โฮสต์ที่ OSM ฝรั่งเศส"), @@ -190,7 +195,7 @@ class MessageLookup extends MessageLookupByLibrary { "invalidKey": MessageLookupByLibrary.simpleMessage("รหัสไม่ถูกต้อง"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "คีย์การกู้คืนที่คุณป้อนไม่ถูกต้อง โปรดตรวจสอบให้แน่ใจว่ามี 24 คำ และตรวจสอบการสะกดของแต่ละคำ\n\nหากคุณป้อนรหัสกู้คืนที่เก่ากว่า ตรวจสอบให้แน่ใจว่ามีความยาว 64 ตัวอักษร และตรวจสอบแต่ละตัวอักษร"), - "itemCount": m42, + "itemCount": m43, "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("กรุณาช่วยเราด้วยข้อมูลนี้"), "lastUpdated": MessageLookupByLibrary.simpleMessage("อัปเดตล่าสุด"), @@ -204,12 +209,15 @@ class MessageLookup extends MessageLookupByLibrary { "logInLabel": MessageLookupByLibrary.simpleMessage("เข้าสู่ระบบ"), "loginTerms": MessageLookupByLibrary.simpleMessage( "โดยการคลิกเข้าสู่ระบบ ฉันยอมรับเงื่อนไขการให้บริการและนโยบายความเป็นส่วนตัว"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "manageParticipants": MessageLookupByLibrary.simpleMessage("จัดการ"), "map": MessageLookupByLibrary.simpleMessage("แผนที่"), "maps": MessageLookupByLibrary.simpleMessage("แผนที่"), "moderateStrength": MessageLookupByLibrary.simpleMessage("ปานกลาง"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("ย้ายไปยังอัลบั้ม"), "name": MessageLookupByLibrary.simpleMessage("ชื่อ"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newest": MessageLookupByLibrary.simpleMessage("ใหม่สุด"), "noRecoveryKey": MessageLookupByLibrary.simpleMessage("ไม่มีคีย์การกู้คืน?"), @@ -218,6 +226,9 @@ class MessageLookup extends MessageLookupByLibrary { "ok": MessageLookupByLibrary.simpleMessage("ตกลง"), "onEnte": MessageLookupByLibrary.simpleMessage( "บน ente"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), "oops": MessageLookupByLibrary.simpleMessage("อ๊ะ"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("อ๊ะ มีบางอย่างผิดพลาด"), @@ -228,7 +239,7 @@ class MessageLookup extends MessageLookupByLibrary { "password": MessageLookupByLibrary.simpleMessage("รหัสผ่าน"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("เปลี่ยนรหัสผ่านสำเร็จ"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordWarning": MessageLookupByLibrary.simpleMessage( "เราไม่จัดเก็บรหัสผ่านนี้ ดังนั้นหากคุณลืม เราจะไม่สามารถถอดรหัสข้อมูลของคุณ"), "peopleUsingYourCode": @@ -245,6 +256,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("สร้างลิงก์สาธารณะแล้ว"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("เปิดใช้ลิงก์สาธารณะแล้ว"), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("กู้คืน"), "recoverAccount": MessageLookupByLibrary.simpleMessage("กู้คืนบัญชี"), "recoverButton": MessageLookupByLibrary.simpleMessage("กู้คืน"), @@ -296,11 +309,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "มีบางอย่างผิดพลาด โปรดลองอีกครั้ง"), "sorry": MessageLookupByLibrary.simpleMessage("ขออภัย"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "status": MessageLookupByLibrary.simpleMessage("สถานะ"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("ครอบครัว"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("คุณ"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("แข็งแรง"), "syncStopped": MessageLookupByLibrary.simpleMessage("หยุดการซิงค์แล้ว"), "syncing": MessageLookupByLibrary.simpleMessage("กำลังซิงค์..."), @@ -341,7 +356,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ดูคีย์การกู้คืน"), "waitingForWifi": MessageLookupByLibrary.simpleMessage("กำลังรอ WiFi..."), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("อ่อน"), "welcomeBack": MessageLookupByLibrary.simpleMessage("ยินดีต้อนรับกลับมา!"), diff --git a/mobile/lib/generated/intl/messages_ti.dart b/mobile/lib/generated/intl/messages_ti.dart index 775cc78213..4968a570e4 100644 --- a/mobile/lib/generated/intl/messages_ti.dart +++ b/mobile/lib/generated/intl/messages_ti.dart @@ -21,5 +21,21 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ti'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => {}; + static Map _notInlinedMessages(_) => { + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups") + }; } diff --git a/mobile/lib/generated/intl/messages_tr.dart b/mobile/lib/generated/intl/messages_tr.dart index a6f1dd9aba..3d3b2a8d09 100644 --- a/mobile/lib/generated/intl/messages_tr.dart +++ b/mobile/lib/generated/intl/messages_tr.dart @@ -22,13 +22,24 @@ class MessageLookup extends MessageLookupByLibrary { static String m0(title) => "${title} (Ben)"; + static String m1(count) => + "${Intl.plural(count, zero: 'Ortak çalışan ekle', one: 'Ortak çalışan ekle', other: 'Ortak çalışan ekle')}"; + + static String m2(count) => + "${Intl.plural(count, one: 'Öğe ekle', other: 'Öğeler ekle')}"; + static String m3(storageAmount, endDate) => "${storageAmount} eklentiniz ${endDate} tarihine kadar geçerlidir"; + static String m4(count) => + "${Intl.plural(count, zero: 'Görüntüleyen ekle', one: 'Görüntüleyen ekle', other: 'Görüntüleyen ekle')}"; + static String m5(emailOrName) => "${emailOrName} tarafından eklendi"; static String m6(albumName) => "${albumName} albümüne başarıyla eklendi"; + static String m7(name) => "${name}\'e hayran kalmak"; + static String m8(count) => "${Intl.plural(count, zero: 'Katılımcı Yok', one: '1 Katılımcı', other: '${count} Katılımcı')}"; @@ -37,6 +48,8 @@ class MessageLookup extends MessageLookupByLibrary { static String m10(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} ücretsiz"; + static String m11(name) => "${name} ile güzel manzaralar"; + static String m12(paymentProvider) => "Lütfen önce mevcut aboneliğinizi ${paymentProvider} adresinden iptal edin"; @@ -85,171 +98,229 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} dosyalar, ${formattedSize} her biri"; - static String m27(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; + static String m27(name) => "Bu e-posta zaten ${name} kişisine bağlı."; - static String m28(email) => "${email} bir Ente hesabına sahip değil"; + static String m28(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; - static String m29(email) => + static String m29(email) => "${email} bir Ente hesabına sahip değil"; + + static String m30(email) => "${email}, Ente hesabı bulunmamaktadır.\n\nOnlarla fotoğraf paylaşımı için bir davet gönder."; - static String m31(text) => "${text} için ekstra fotoğraflar bulundu"; + static String m31(name) => "${name}\'e sarılmak"; - static String m33(count, formattedNumber) => - "Bu cihazdaki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; + static String m32(text) => "${text} için ekstra fotoğraflar bulundu"; + + static String m33(name) => "${name} ile ziyafet"; static String m34(count, formattedNumber) => + "Bu cihazdaki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; + + static String m35(count, formattedNumber) => "Bu albümdeki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "Birisinin davet kodunuzu uygulayıp ücretli hesap açtığı her seferede ${storageAmountInGB} GB"; - static String m36(endDate) => "Ücretsiz deneme ${endDate} sona erir"; + static String m37(endDate) => "Ücretsiz deneme ${endDate} sona erir"; - static String m38(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; + static String m38(count) => + "Aktif bir aboneliğiniz olduğu sürece Ente üzerinden ${Intl.plural(count, one: 'ona', other: 'onlara')} hâlâ erişebilirsiniz"; - static String m40(currentlyProcessing, totalCount) => + static String m39(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; + + static String m40(count, formattedSize) => + "${Intl.plural(count, one: 'Cihazdan silinerek ${formattedSize} boşaltılabilir', other: 'Cihazdan silinerek ${formattedSize} boşaltılabilirler')}"; + + static String m41(currentlyProcessing, totalCount) => "Siliniyor ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m42(name) => "${name} ile doğa yürüyüşü"; + + static String m43(count) => "${Intl.plural(count, one: '${count} öğe', other: '${count} öğeler')}"; - static String m44(email) => + static String m44(name) => "${name} ile son an"; + + static String m45(email) => "${email} sizi güvenilir bir kişi olmaya davet etti"; - static String m45(expiryTime) => - "Bu bağlantı ${expiryTime} dan sonra geçersiz olacaktır"; + static String m46(expiryTime) => + "Bu bağlantı ${expiryTime} tarihinden itibaren geçersiz olacaktır"; - static String m46(email) => "Kişiyi ${email} adresine bağlayın"; + static String m47(email) => "Kişiyi ${email} adresine bağlayın"; - static String m47(personName, email) => + static String m48(personName, email) => "Bu, ${personName} ile ${email} arasında bağlantı kuracaktır."; - static String m50(albumName) => "${albumName} adlı albüme başarıyla taşındı"; + static String m49(count, formattedCount) => + "${Intl.plural(count, zero: 'hiç anı yok', one: '${formattedCount} anı', other: '${formattedCount} anı')}"; - static String m51(personName) => "${personName} için öneri yok"; + static String m50(count) => + "${Intl.plural(count, one: 'Öğeyi taşı', other: 'Öğeleri taşı')}"; - static String m52(name) => "${name} değil mi?"; + static String m51(albumName) => "${albumName} adlı albüme başarıyla taşındı"; - static String m53(familyAdminEmail) => + static String m52(personName) => "${personName} için öneri yok"; + + static String m53(name) => "${name} değil mi?"; + + static String m54(familyAdminEmail) => "Kodunuzu değiştirmek için lütfen ${familyAdminEmail} ile iletişime geçin."; - static String m55(passwordStrengthValue) => + static String m55(name) => "${name} ile parti"; + + static String m56(passwordStrengthValue) => "Şifrenin güçlülük seviyesi: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Sizden ücret alındıysa lütfen ${providerName} destek ekibiyle görüşün"; - static String m59(count) => + static String m58(name, age) => "${name} ${age} yaşında!"; + + static String m59(name, age) => "${name} yakında ${age} yaşına giriyor"; + + static String m60(count) => "${Intl.plural(count, zero: 'Fotoğraf yok', one: '1 fotoğraf', other: '${count} fotoğraf')}"; - static String m61(endDate) => + static String m61(count) => + "${Intl.plural(count, zero: '0 fotoğraf', one: '1 fotoğraf', other: '${count} fotoğraf')}"; + + static String m62(endDate) => "Ücretsiz deneme süresi ${endDate} tarihine kadar geçerlidir.\nDaha sonra ücretli bir plan seçebilirsiniz."; - static String m62(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; + static String m63(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; - static String m63(toEmail) => - "Lütfen günlükleri şu adrese gönderin\n${toEmail}"; + static String m64(toEmail) => + "Lütfen kayıtları şu adrese gönderin\n${toEmail}"; - static String m65(folderName) => "İşleniyor ${folderName}..."; + static String m65(name) => "${name} ile poz verme"; - static String m66(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; + static String m66(folderName) => "İşleniyor ${folderName}..."; - static String m67(name) => "Sizi ${name}\'e yeniden atadım"; + static String m67(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; - static String m68(days, email) => + static String m68(name) => "Sizi ${name}\'e yeniden atadı"; + + static String m69(days, email) => "Hesabınıza ${days} gün sonra erişebilirsiniz. ${email} adresine bir bildirim gönderilecektir."; - static String m69(email) => + static String m70(email) => "Artık yeni bir parola belirleyerek ${email} hesabını kurtarabilirsiniz."; - static String m70(email) => "${email} hesabınızı kurtarmaya çalışıyor."; + static String m71(email) => "${email} hesabınızı kurtarmaya çalışıyor."; - static String m71(storageInGB) => "3. Hepimiz ${storageInGB} GB* bedava alın"; + static String m72(storageInGB) => "3. İkinizde bedava ${storageInGB} GB alın"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} bu paylaşılan albümden kaldırılacaktır\n\nOnlar tarafından eklenen tüm fotoğraflar da albümden kaldırılacaktır"; - static String m73(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; + static String m74(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; + + static String m75(name) => "${name} ile yolculuk"; + + static String m76(count) => + "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; + + static String m77(snapshotLength, searchLength) => + "Bölüm uzunluğu uyuşmazlığı: ${snapshotLength} != ${searchLength}"; + + static String m78(count) => "${count} seçildi"; + + static String m79(count, yourCount) => + "Seçilenler: ${count} (${yourCount} sizin seçiminiz)"; + + static String m80(name) => "${name} ile selfieler"; + + static String m81(verificationID) => + "İşte ente.io için doğrulama kimliğim: ${verificationID}."; + + static String m82(verificationID) => + "Merhaba, bu ente.io doğrulama kimliğinizin doğruluğunu onaylayabilir misiniz: ${verificationID}"; + + static String m83(referralCode, referralStorageInGB) => + "Ente davet kodu: ${referralCode} \n\nÜcretli hesaba başvurduktan sonra ${referralStorageInGB} GB bedava almak için \nAyarlar → Genel → Davetlerde bu kodu girin\n\nhttps://ente.io"; + + static String m84(numberOfPeople) => + "${Intl.plural(numberOfPeople, zero: 'Belirli kişilerle paylaş', one: '1 kişiyle paylaşıldı', other: '${numberOfPeople} kişiyle paylaşıldı')}"; + + static String m85(emailIDs) => "${emailIDs} ile paylaşıldı"; + + static String m86(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; + + static String m87(fileType) => + "${fileType} Ente ve cihazınızdan silinecektir."; + + static String m88(fileType) => "${fileType} Ente\'den silinecektir."; + + static String m89(name) => "${name} ile spor"; + + static String m90(name) => "Sahne ${name}\'in"; + + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; + + static String m92( + usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => + "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} kullanıldı"; + + static String m93(id) => + "${id}\'niz zaten başka bir ente hesabına bağlı.\n${id} numaranızı bu hesapla kullanmak istiyorsanız lütfen desteğimizle iletişime geçin\'\'"; + + static String m94(endDate) => + "Aboneliğiniz ${endDate} tarihinde iptal edilecektir"; + + static String m95(completed, total) => "${completed}/${total} anı korundu"; + + static String m96(ignoreReason) => + "Yüklemek için dokunun, yükleme şu anda ${ignoreReason} nedeniyle yok sayılıyor"; + + static String m97(storageAmountInGB) => + "Aynı zamanda ${storageAmountInGB} GB alıyorlar"; + + static String m98(email) => "Bu, ${email}\'in Doğrulama Kimliği"; + + static String m99(count) => + "${Intl.plural(count, one: 'Bu hafta, ${count} yıl önce', other: 'Bu hafta, ${count} yıl önce')}"; + + static String m100(dateFormat) => "${dateFormat} yıllar boyunca"; + + static String m101(count) => + "${Intl.plural(count, zero: 'Yakında', one: '1 gün', other: '${count} gün')}"; + + static String m102(year) => "${year} yılındaki gezi"; + + static String m103(location) => "${location}\'a gezi"; + + static String m104(email) => + "${email} ile eski bir irtibat kişisi olmaya davet edildiniz."; + + static String m105(galleryType) => + "Galeri türü ${galleryType} yeniden adlandırma için desteklenmiyor"; + + static String m106(ignoreReason) => + "Yükleme ${ignoreReason} nedeniyle yok sayıldı"; + + static String m107(count) => "${count} anı korunuyor..."; + + static String m108(endDate) => "${endDate} tarihine kadar geçerli"; + + static String m109(email) => "${email} doğrula"; + + static String m110(name) => + "Bağlantıyı kaldırmak için ${name} kişisini görüntüle"; + + static String m111(count) => + "${Intl.plural(count, zero: '0 izleyici eklendi', one: '1 izleyici eklendi', other: '${count} izleyici eklendi')}"; + + static String m112(email) => + "E-postayı ${email} adresine gönderdik"; static String m113(count) => "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; - static String m75(snapshotLength, searchLength) => - "Bölüm uzunluğu uyuşmazlığı: ${snapshotLength} != ${searchLength}"; + static String m114(name) => "Sen ve ${name}"; - static String m76(count) => "${count} seçildi"; - - static String m77(count, yourCount) => - "Seçilenler: ${count} (${yourCount} sizin seçiminiz)"; - - static String m79(verificationID) => - "İşte ente.io için doğrulama kimliğim: ${verificationID}."; - - static String m80(verificationID) => - "Merhaba, bu ente.io doğrulama kimliğinizin doğruluğunu onaylayabilir misiniz: ${verificationID}"; - - static String m81(referralCode, referralStorageInGB) => - "Ente davet kodu: ${referralCode} \n\nÜcretli hesaba başvurduktan sonra ${referralStorageInGB} GB bedava almak için \nAyarlar → Genel → Davetlerde bu kodu girin\n\nhttps://ente.io"; - - static String m82(numberOfPeople) => - "${Intl.plural(numberOfPeople, zero: 'Belirli kişilerle paylaş', one: '1 kişiyle paylaşıldı', other: '${numberOfPeople} kişiyle paylaşıldı')}"; - - static String m83(emailIDs) => "${emailIDs} ile paylaşıldı"; - - static String m84(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; - - static String m85(fileType) => - "${fileType} Ente ve cihazınızdan silinecektir."; - - static String m86(fileType) => "${fileType} Ente\'den silinecektir."; - - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; - - static String m90( - usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => - "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} kullanıldı"; - - static String m91(id) => - "${id}\'niz zaten başka bir ente hesabına bağlı.\n${id} numaranızı bu hesapla kullanmak istiyorsanız lütfen desteğimizle iletişime geçin\'\'"; - - static String m92(endDate) => - "Aboneliğiniz ${endDate} tarihinde iptal edilecektir"; - - static String m93(completed, total) => "${completed}/${total} anı korundu"; - - static String m94(ignoreReason) => - "Yüklemek için dokunun, yükleme şu anda ${ignoreReason} nedeniyle yok sayılıyor"; - - static String m95(storageAmountInGB) => - "Aynı zamanda ${storageAmountInGB} GB alıyorlar"; - - static String m96(email) => "Bu, ${email}\'in Doğrulama Kimliği"; - - static String m99(count) => - "${Intl.plural(count, zero: 'yakında', one: '1 gün', other: '${count} gün')}"; - - static String m102(email) => - "${email} ile eski bir irtibat kişisi olmaya davet edildiniz."; - - static String m103(galleryType) => - "Galeri türü ${galleryType} yeniden adlandırma için desteklenmiyor"; - - static String m104(ignoreReason) => - "Yükleme ${ignoreReason} nedeniyle yok sayıldı"; - - static String m105(count) => "${count} anı korunuyor..."; - - static String m106(endDate) => "${endDate} tarihine kadar geçerli"; - - static String m107(email) => "${email} doğrula"; - - static String m109(email) => - "E-postayı ${email} adresine gönderdik"; - - static String m110(count) => - "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; - - static String m112(storageSaved) => + static String m115(storageSaved) => "Başarılı bir şekilde ${storageSaved} alanını boşalttınız!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -275,8 +346,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yeni e-posta ekle"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Düzenleyici ekle"), + "addCollaborators": m1, "addFiles": MessageLookupByLibrary.simpleMessage("Dosyaları Ekle"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Cihazdan ekle"), + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Konum Ekle"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Ekle"), "addMore": MessageLookupByLibrary.simpleMessage("Daha fazla ekle"), @@ -298,6 +371,7 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Güvenilir kişi ekle"), "addViewer": MessageLookupByLibrary.simpleMessage("Görüntüleyici ekle"), + "addViewers": m4, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Fotoğraflarınızı şimdi ekleyin"), "addedAs": MessageLookupByLibrary.simpleMessage("Eklendi"), @@ -305,6 +379,7 @@ class MessageLookup extends MessageLookupByLibrary { "addedSuccessfullyTo": m6, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Favorilere ekleniyor..."), + "admiringThem": m7, "advanced": MessageLookupByLibrary.simpleMessage("Gelişmiş"), "advancedSettings": MessageLookupByLibrary.simpleMessage("Gelişmiş"), "after1Day": MessageLookupByLibrary.simpleMessage("1 gün sonra"), @@ -318,7 +393,7 @@ class MessageLookup extends MessageLookupByLibrary { "albumUpdated": MessageLookupByLibrary.simpleMessage("Albüm güncellendi"), "albums": MessageLookupByLibrary.simpleMessage("Albümler"), - "allClear": MessageLookupByLibrary.simpleMessage("✨ Tamamen temizle"), + "allClear": MessageLookupByLibrary.simpleMessage("✨ Tümü temizlendi"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("Tüm anılar saklandı"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( @@ -327,7 +402,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bu, gruptaki ilk fotoğraftır. Seçilen diğer fotoğraflar otomatik olarak bu yeni tarihe göre kaydırılacaktır"), "allow": MessageLookupByLibrary.simpleMessage("İzin ver"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( - "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver."), + "Bağlantıya sahip olan kişilerin paylaşılan albüme fotoğraf eklemelerine izin ver."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Fotoğraf eklemeye izin ver"), "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( @@ -366,11 +441,11 @@ class MessageLookup extends MessageLookupByLibrary { "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Cihazınızın varsayılan kilit ekranı ile PIN veya parola içeren özel bir kilit ekranı arasında seçim yapın."), "appVersion": m9, - "appleId": MessageLookupByLibrary.simpleMessage("Apple kimliği"), + "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Uygula"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Kodu girin"), "appstoreSubscription": - MessageLookupByLibrary.simpleMessage("PlayStore aboneliği"), + MessageLookupByLibrary.simpleMessage("AppStore aboneliği"), "archive": MessageLookupByLibrary.simpleMessage("Arşiv"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Albümü arşivle"), "archiving": MessageLookupByLibrary.simpleMessage("Arşivleniyor..."), @@ -383,18 +458,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Planı değistirmek istediğinize emin misiniz?"), "areYouSureYouWantToExit": MessageLookupByLibrary.simpleMessage( - "Çıkmak istediğinden emin misin?"), + "Çıkmak istediğinden emin misiniz?"), "areYouSureYouWantToLogout": MessageLookupByLibrary.simpleMessage( "Çıkış yapmak istediğinize emin misiniz?"), "areYouSureYouWantToRenew": MessageLookupByLibrary.simpleMessage( "Yenilemek istediğinize emin misiniz?"), "areYouSureYouWantToResetThisPerson": MessageLookupByLibrary.simpleMessage( - "Bu kişiyi sıfırlamak istediğinden emin misin?"), + "Bu kişiyi sıfırlamak istediğinden emin misiniz?"), "askCancelReason": MessageLookupByLibrary.simpleMessage( "Aboneliğiniz iptal edilmiştir. Bunun sebebini paylaşmak ister misiniz?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( - "Hesabınızı neden silmek istiyorsunuz?"), + "Hesabınızı silme sebebiniz nedir?"), "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage( "Sevdiklerinizden paylaşmalarını isteyin"), "atAFalloutShelter": @@ -440,7 +515,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ayarlar\'da Ente Photos uygulaması için Yerel Ağ izinlerinin açık olduğundan emin olun."), "autoLock": MessageLookupByLibrary.simpleMessage("Otomatik Kilit"), "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Uygulamayı arka plana attıktan sonra kilitlendiği süre"), + "Uygulama arka plana geçtikten sonra kilitleneceği süre"), "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( "Teknik aksaklık nedeniyle oturumunuz kapatıldı. Verdiğimiz rahatsızlıktan dolayı özür dileriz."), "autoPair": MessageLookupByLibrary.simpleMessage("Otomatik eşle"), @@ -450,6 +525,7 @@ class MessageLookup extends MessageLookupByLibrary { "availableStorageSpace": m10, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Yedeklenmiş klasörler"), + "backgroundWithThem": m11, "backup": MessageLookupByLibrary.simpleMessage("Yedekle"), "backupFailed": MessageLookupByLibrary.simpleMessage("Yedekleme başarısız oldu"), @@ -463,19 +539,15 @@ class MessageLookup extends MessageLookupByLibrary { "backupStatusDescription": MessageLookupByLibrary.simpleMessage( "Eklenen öğeler burada görünecek"), "backupVideos": - MessageLookupByLibrary.simpleMessage("Videolari yedekle"), + MessageLookupByLibrary.simpleMessage("Videoları yedekle"), + "beach": MessageLookupByLibrary.simpleMessage("Kum ve deniz"), "birthday": MessageLookupByLibrary.simpleMessage("Doğum Günü"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Muhteşem Cuma kampanyası"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), - "cLFamilyPlan": - MessageLookupByLibrary.simpleMessage("Aile Planı Sınırları"), - "cLFamilyPlanDesc": MessageLookupByLibrary.simpleMessage( - "Artık aile üyelerinizin ne kadar depolama alanı kullanabileceğine dair sınırlar belirleyebilirsiniz."), - "cLIcon": MessageLookupByLibrary.simpleMessage("Yeni Simge"), - "cLMemories": MessageLookupByLibrary.simpleMessage("Anılar"), "cachedData": - MessageLookupByLibrary.simpleMessage("Ön belleğe alınan veri"), + MessageLookupByLibrary.simpleMessage("Önbelleğe alınmış veriler"), "calculating": MessageLookupByLibrary.simpleMessage("Hesaplanıyor..."), "canNotOpenBody": MessageLookupByLibrary.simpleMessage( "Üzgünüz, Bu albüm uygulama içinde açılamadı."), @@ -489,7 +561,7 @@ class MessageLookup extends MessageLookupByLibrary { "Yalnızca size ait dosyalar için bağlantı oluşturabilir"), "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Yalnızca size ait dosyaları kaldırabilir"), - "cancel": MessageLookupByLibrary.simpleMessage("İptal Et"), + "cancel": MessageLookupByLibrary.simpleMessage("İptal et"), "cancelAccountRecovery": MessageLookupByLibrary.simpleMessage("Kurtarma işlemini iptal et"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( @@ -529,9 +601,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Durumu kontrol edin"), "checking": MessageLookupByLibrary.simpleMessage("Kontrol ediliyor..."), "checkingModels": MessageLookupByLibrary.simpleMessage( - "Modelleri kontrol ediyorum..."), + "Modeller kontrol ediliyor..."), + "city": MessageLookupByLibrary.simpleMessage("Şehirde"), "claimFreeStorage": - MessageLookupByLibrary.simpleMessage("Bedava alan talep edin"), + MessageLookupByLibrary.simpleMessage("Bedava alan kazanın"), "claimMore": MessageLookupByLibrary.simpleMessage("Arttır!"), "claimed": MessageLookupByLibrary.simpleMessage("Alındı"), "claimedStorageSoFar": m14, @@ -540,11 +613,14 @@ class MessageLookup extends MessageLookupByLibrary { "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( "Diğer albümlerde bulunan Kategorilenmemiş tüm dosyaları kaldırın"), "clearCaches": - MessageLookupByLibrary.simpleMessage("Önbellekleri temizle"), - "clearIndexes": MessageLookupByLibrary.simpleMessage("Açık Dizin"), + MessageLookupByLibrary.simpleMessage("Önbelleği temizle"), + "clearIndexes": + MessageLookupByLibrary.simpleMessage("Dizinleri temizle"), "click": MessageLookupByLibrary.simpleMessage("• Tıklamak"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Taşma menüsüne tıklayın"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Kapat"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage( "Yakalama zamanına göre kulüp"), @@ -563,7 +639,7 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "Ente aplikasyonu veya hesabı olmadan insanların paylaşılan albümde fotoğraf ekleyip görüntülemelerine izin vermek için bir bağlantı oluşturun. Grup veya etkinlik fotoğraflarını toplamak için harika bir seçenek."), "collaborativeLink": - MessageLookupByLibrary.simpleMessage("Organizasyon bağlantısı"), + MessageLookupByLibrary.simpleMessage("Ortak bağlantı"), "collaborativeLinkCreatedFor": m15, "collaborator": MessageLookupByLibrary.simpleMessage("Düzenleyici"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": @@ -612,7 +688,7 @@ class MessageLookup extends MessageLookupByLibrary { "convertToAlbum": MessageLookupByLibrary.simpleMessage("Albüme taşı"), "copyEmailAddress": MessageLookupByLibrary.simpleMessage("E-posta adresini kopyala"), - "copyLink": MessageLookupByLibrary.simpleMessage("Linki kopyala"), + "copyLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı kopyala"), "copypasteThisCodentoYourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( "Bu kodu kopyalayın ve kimlik doğrulama uygulamanıza yapıştırın"), @@ -637,18 +713,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yeni bir hesap oluşturun"), "createOrSelectAlbum": MessageLookupByLibrary.simpleMessage("Albüm oluştur veya seç"), - "createPublicLink": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), + "createPublicLink": MessageLookupByLibrary.simpleMessage( + "Herkese açık bir bağlantı oluştur"), "creatingLink": MessageLookupByLibrary.simpleMessage("Bağlantı oluşturuluyor..."), "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage("Kritik güncelleme mevcut"), "crop": MessageLookupByLibrary.simpleMessage("Kırp"), + "curatedMemories": + MessageLookupByLibrary.simpleMessage("Seçilmiş anılar"), "currentUsageIs": MessageLookupByLibrary.simpleMessage("Güncel kullanımınız "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("şu anda çalışıyor"), - "custom": MessageLookupByLibrary.simpleMessage("Kişisel"), + "custom": MessageLookupByLibrary.simpleMessage("Özel"), "customEndpoint": m20, "darkTheme": MessageLookupByLibrary.simpleMessage("Karanlık"), "dayToday": MessageLookupByLibrary.simpleMessage("Bugün"), @@ -664,7 +742,7 @@ class MessageLookup extends MessageLookupByLibrary { "delete": MessageLookupByLibrary.simpleMessage("Sil"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Hesabı sil"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( - "Aramızdan ayrıldığınız için üzgünüz. Lütfen kendimizi geliştirmemize yardımcı olun. Neden ayrıldığınızı Açıklar mısınız."), + "Gittiğini gördüğümüze üzüldük. Lütfen gelişmemize yardımcı olmak için neden ayrıldığınızı açıklayın."), "deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage("Hesabımı kalıcı olarak sil"), "deleteAlbum": MessageLookupByLibrary.simpleMessage("Albümü sil"), @@ -676,11 +754,11 @@ class MessageLookup extends MessageLookupByLibrary { "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır. Tüm Ente uygulamalarına yüklediğiniz veriler ve hesabınız kalıcı olarak silinecektir."), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( - "Lütfen kayıtlı e-posta adresinizden account-deletion@ente.io\'a e-posta gönderiniz."), + "Lütfen kayıtlı e-posta adresinizden account-deletion@ente.io\'ya e-posta gönderiniz."), "deleteEmptyAlbums": MessageLookupByLibrary.simpleMessage("Boş albümleri sil"), "deleteEmptyAlbumsWithQuestionMark": - MessageLookupByLibrary.simpleMessage("Boş albümleri sileyim mi?"), + MessageLookupByLibrary.simpleMessage("Boş albümler silinsin mi?"), "deleteFromBoth": MessageLookupByLibrary.simpleMessage("Her ikisinden de sil"), "deleteFromDevice": @@ -776,10 +854,11 @@ class MessageLookup extends MessageLookupByLibrary { "duplicateFileCountWithStorageSaved": m25, "duplicateItemsGroup": m26, "edit": MessageLookupByLibrary.simpleMessage("Düzenle"), + "editEmailAlreadyLinked": m27, "editLocation": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), - "editPerson": MessageLookupByLibrary.simpleMessage("Kişiyi Düzenle"), + "editPerson": MessageLookupByLibrary.simpleMessage("Kişiyi düzenle"), "editTime": MessageLookupByLibrary.simpleMessage("Zamanı düzenle"), "editsSaved": MessageLookupByLibrary.simpleMessage("Düzenleme kaydedildi"), @@ -790,15 +869,16 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-Posta"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Bu e-posta adresi zaten kayıtlı."), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Bu e-posta adresi sistemde kayıtlı değil."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-posta doğrulama"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( - "Günlüklerinizi e-postayla gönderin"), + "Kayıtlarınızı e-postayla gönderin"), + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage( "Acil Durum İletişim Bilgileri"), "empty": MessageLookupByLibrary.simpleMessage("Boşalt"), @@ -806,7 +886,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Çöp kutusu boşaltılsın mı?"), "enable": MessageLookupByLibrary.simpleMessage("Etkinleştir"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( - "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde makine öğrenimini destekler"), + "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde çalışan makine öğrenimini kullanır"), "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( "Sihirli arama ve yüz tanıma için makine öğrenimini etkinleştirin"), "enableMaps": @@ -836,7 +916,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bir albüm adı girin"), "enterCode": MessageLookupByLibrary.simpleMessage("Kodu giriniz"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( - "Arkadaşınız tarafından sağlanan kodu girerek hem sizin hem de arkadaşınızın ücretsiz depolamayı talep etmek için girin"), + "İkiniz için de ücretsiz depolama alanı talep etmek için arkadaşınız tarafından sağlanan kodu girin"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("Doğum Günü (isteğe bağlı)"), "enterEmail": @@ -862,6 +942,8 @@ class MessageLookup extends MessageLookupByLibrary { "Lütfen geçerli bir E-posta adresi girin."), "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage("E-posta adresinizi girin"), + "enterYourNewEmailAddress": MessageLookupByLibrary.simpleMessage( + "Yeni e-posta adresinizi girin"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Lütfen şifrenizi giriniz"), "enterYourRecoveryKey": @@ -879,7 +961,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Veriyi dışarı aktar"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Ekstra fotoğraflar bulundu"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Yüz henüz kümelenmedi, lütfen daha sonra tekrar gelin"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Yüz Tanıma"), @@ -911,9 +993,10 @@ class MessageLookup extends MessageLookupByLibrary { "Ekstra ödeme yapmadan mevcut planınıza 5 aile üyesi ekleyin.\n\nHer üyenin kendine ait özel alanı vardır ve paylaşılmadıkça birbirlerinin dosyalarını göremezler.\n\nAile planları ücretli ente aboneliğine sahip müşteriler tarafından kullanılabilir.\n\nBaşlamak için şimdi abone olun!"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("Aile"), "familyPlans": MessageLookupByLibrary.simpleMessage("Aile Planı"), - "faq": MessageLookupByLibrary.simpleMessage("Sıkça sorulan sorular"), - "faqs": MessageLookupByLibrary.simpleMessage("Sık sorulanlar"), + "faq": MessageLookupByLibrary.simpleMessage("Sık sorulan sorular"), + "faqs": MessageLookupByLibrary.simpleMessage("Sık Sorulan Sorular"), "favorite": MessageLookupByLibrary.simpleMessage("Favori"), + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("Geri Bildirim"), "file": MessageLookupByLibrary.simpleMessage("Dosya"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( @@ -927,58 +1010,63 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dosya türü"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dosya türleri ve adları"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dosyalar silinmiş"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( "Dosyalar galeriye kaydedildi"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Kişileri isimlere göre çabucak bulun"), + "Kişileri isimlerine göre bulun"), "findThemQuickly": - MessageLookupByLibrary.simpleMessage("Onları çabucak bulun"), + MessageLookupByLibrary.simpleMessage("Çabucak bulun"), "flip": MessageLookupByLibrary.simpleMessage("Çevir"), + "food": MessageLookupByLibrary.simpleMessage("Yemek keyfi"), "forYourMemories": - MessageLookupByLibrary.simpleMessage("anıların için"), + MessageLookupByLibrary.simpleMessage("anılarınız için"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Şifremi unuttum"), "foundFaces": MessageLookupByLibrary.simpleMessage("Yüzler bulundu"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Alınan bedava alan"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Kullanılabilir bedava alan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Ücretsiz deneme"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Cihaz alanını boşaltın"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Zaten yedeklenmiş dosyaları temizleyerek cihazınızda yer kazanın."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Boş alan"), + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("Galeri"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Galeride 1000\'e kadar anı gösterilir"), "general": MessageLookupByLibrary.simpleMessage("Genel"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Şifreleme anahtarı oluşturuluyor..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Ayarlara git"), - "googlePlayId": - MessageLookupByLibrary.simpleMessage("Google play kimliği"), + "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( "Lütfen Ayarlar uygulamasında tüm fotoğraflara erişime izin verin"), "grantPermission": MessageLookupByLibrary.simpleMessage("İzinleri değiştir"), + "greenery": MessageLookupByLibrary.simpleMessage("Yeşil yaşam"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage( "Yakındaki fotoğrafları gruplandır"), "guestView": MessageLookupByLibrary.simpleMessage("Misafir Görünümü"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Misafir görünümünü etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( - "Ente\'yi nereden duydunuz? (opsiyonel)"), + "Ente\'yi nereden duydunuz? (isteğe bağlı)"), "help": MessageLookupByLibrary.simpleMessage("Yardım"), "hidden": MessageLookupByLibrary.simpleMessage("Gizle"), "hide": MessageLookupByLibrary.simpleMessage("Gizle"), @@ -990,6 +1078,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( "Paylaşılan öğeleri ana galeriden gizle"), "hiding": MessageLookupByLibrary.simpleMessage("Gizleniyor..."), + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Fransa\'da ağırlandı"), "howItWorks": MessageLookupByLibrary.simpleMessage("Nasıl çalışır"), @@ -1019,9 +1108,9 @@ class MessageLookup extends MessageLookupByLibrary { "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("Yanlış kurtarma kodu"), "indexedItems": - MessageLookupByLibrary.simpleMessage("Yeni öğeleri indeksle"), + MessageLookupByLibrary.simpleMessage("Dizinlenmiş öğeler"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "İndeksleme duraklatılmıştır. Cihaz hazır olduğunda otomatik olarak devam edecektir."), + "Dizin oluşturma duraklatıldı. Cihaz hazır olduğunda otomatik olarak devam edecektir."), "ineligible": MessageLookupByLibrary.simpleMessage("Uygun Değil"), "info": MessageLookupByLibrary.simpleMessage("Bilgi"), "insecureDevice": @@ -1047,10 +1136,10 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( - "Öğeler, kalıcı olarak silinmeden önce kalan gün sayısını gösterir"), + "Öğeler kalıcı olarak silinmeden önce kalan gün sayısını gösterir"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Seçilen öğeler bu albümden kaldırılacak"), "join": MessageLookupByLibrary.simpleMessage("Katıl"), @@ -1068,9 +1157,12 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Lütfen bu bilgilerle bize yardımcı olun"), "language": MessageLookupByLibrary.simpleMessage("Dil"), + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("En son güncellenen"), - "leave": MessageLookupByLibrary.simpleMessage("Çıkış yap"), + "lastYearsTrip": + MessageLookupByLibrary.simpleMessage("Geçen yılki gezi"), + "leave": MessageLookupByLibrary.simpleMessage("Ayrıl"), "leaveAlbum": MessageLookupByLibrary.simpleMessage("Albümü yeniden adlandır"), "leaveFamily": @@ -1081,7 +1173,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Geleneksel"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Geleneksel hesaplar"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Geleneksel yol, güvendiğiniz kişilerin yokluğunuzda hesabınıza erişmesine olanak tanır."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1091,28 +1183,28 @@ class MessageLookup extends MessageLookupByLibrary { "link": MessageLookupByLibrary.simpleMessage("Bağlantı"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage("Link panoya kopyalandı"), - "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Cihaz limiti"), + "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Cihaz sınırı"), "linkEmail": MessageLookupByLibrary.simpleMessage("E-posta bağlantısı"), "linkEmailToContactBannerCaption": - MessageLookupByLibrary.simpleMessage("d"), + MessageLookupByLibrary.simpleMessage("daha hızlı paylaşım için"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Geçerli"), "linkExpired": MessageLookupByLibrary.simpleMessage("Süresi dolmuş"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": - MessageLookupByLibrary.simpleMessage("Linkin geçerliliği"), + MessageLookupByLibrary.simpleMessage("Bağlantı geçerliliği"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Bağlantının süresi dolmuş"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Asla"), - "linkPerson": MessageLookupByLibrary.simpleMessage("Bağlantı kişisi"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Kişiyi bağla"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "daha iyi paylaşım deneyimi için"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Canlı Fotoğraf"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi ailenizle paylaşabilirsiniz"), "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Şu ana kadar 30 milyondan fazla anıyı koruduk"), + "Şimdiye kadar 200 milyondan fazla anıyı koruduk"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Verilerinizin 3 kopyasını saklıyoruz, biri yer altı serpinti sığınağında"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1139,7 +1231,7 @@ class MessageLookup extends MessageLookupByLibrary { "Fotoğraflarınız yükleniyor..."), "localGallery": MessageLookupByLibrary.simpleMessage("Yerel galeri"), "localIndexing": - MessageLookupByLibrary.simpleMessage("Yerel indeksleme"), + MessageLookupByLibrary.simpleMessage("Yerel dizinleme"), "localSyncErrorMessage": MessageLookupByLibrary.simpleMessage( "Yerel fotoğraf senkronizasyonu beklenenden daha uzun sürdüğü için bir şeyler ters gitmiş gibi görünüyor. Lütfen destek ekibimize ulaşın"), "location": MessageLookupByLibrary.simpleMessage("Konum"), @@ -1162,19 +1254,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("TOTP ile giriş yap"), "logout": MessageLookupByLibrary.simpleMessage("Çıkış yap"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( - "Bu, sorununuzu gidermemize yardımcı olmak için günlükleri gönderecektir. Belirli dosyalarla ilgili sorunların izlenmesine yardımcı olmak için dosya adlarının ekleneceğini lütfen unutmayın."), + "Bu, sorununuzu gidermemize yardımcı olmak için kayıtları gönderecektir. Belirli dosyalarla ilgili sorunların izlenmesine yardımcı olmak için dosya adlarının ekleneceğini lütfen unutmayın."), "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( "Uçtan uca şifrelemeyi doğrulamak için bir e-postaya uzun basın."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Tam ekranda görüntülemek için bir öğeye uzun basın"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Video Döngüsü Kapalı"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("Video Döngüsü Açık"), "lostDevice": - MessageLookupByLibrary.simpleMessage("Cihazı kayıp mı ettiniz?"), + MessageLookupByLibrary.simpleMessage("Cihazınızı mı kaybettiniz?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Makine öğrenimi"), "magicSearch": MessageLookupByLibrary.simpleMessage("Sihirli arama"), @@ -1182,11 +1276,11 @@ class MessageLookup extends MessageLookupByLibrary { "Sihirli arama, fotoğrafları içeriklerine göre aramanıza olanak tanır, örneğin \'çiçek\', \'kırmızı araba\', \'kimlik belgeleri\'"), "manage": MessageLookupByLibrary.simpleMessage("Yönet"), "manageDeviceStorage": - MessageLookupByLibrary.simpleMessage("Cihaz önbelliğini yönet"), + MessageLookupByLibrary.simpleMessage("Cihaz Önbelliğini Yönet"), "manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage( "Yerel önbellek depolama alanını gözden geçirin ve temizleyin."), "manageFamily": MessageLookupByLibrary.simpleMessage("Aileyi yönet"), - "manageLink": MessageLookupByLibrary.simpleMessage("Linki yönet"), + "manageLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı yönet"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Yönet"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Abonelikleri yönet"), @@ -1197,6 +1291,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("Ben"), + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("Ürünler"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("Var olan ile birleştir."), @@ -1213,7 +1308,7 @@ class MessageLookup extends MessageLookupByLibrary { "mlConsentTitle": MessageLookupByLibrary.simpleMessage( "Makine öğrenimi etkinleştirilsin mi?"), "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Tüm öğeler dizine eklenene kadar makine öğreniminin daha yüksek bant genişliği ve pil kullanımı ile sonuçlanacağını lütfen unutmayın. Daha hızlı indeksleme için masaüstü uygulamasını kullanmayı düşünün, tüm sonuçlar otomatik olarak senkronize edilecektir."), + "Makine öğreniminin, tüm öğeler dizine eklenene kadar daha yüksek bant genişliği ve pil kullanımıyla sonuçlanacağını lütfen unutmayın. Daha hızlı dizinleme için masaüstü uygulamasını kullanmayı deneyin, tüm sonuçlar otomatik olarak senkronize edilir."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobil, Web, Masaüstü"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Ilımlı"), @@ -1223,15 +1318,18 @@ class MessageLookup extends MessageLookupByLibrary { "moments": MessageLookupByLibrary.simpleMessage("Anlar"), "month": MessageLookupByLibrary.simpleMessage("ay"), "monthly": MessageLookupByLibrary.simpleMessage("Aylık"), + "moon": MessageLookupByLibrary.simpleMessage("Ay ışığında"), "moreDetails": MessageLookupByLibrary.simpleMessage("Daha fazla detay"), "mostRecent": MessageLookupByLibrary.simpleMessage("En son"), "mostRelevant": MessageLookupByLibrary.simpleMessage("En alakalı"), + "mountains": MessageLookupByLibrary.simpleMessage("Tepelerin ötesinde"), + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage( "Seçilen fotoğrafları bir tarihe taşıma"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("Albüme taşı"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Gizli albüme ekle"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Cöp kutusuna taşı"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1246,6 +1344,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Yeni albüm"), "newLocation": MessageLookupByLibrary.simpleMessage("Yeni konum"), "newPerson": MessageLookupByLibrary.simpleMessage("Yeni Kişi"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("Yeni aralık"), "newToEnte": MessageLookupByLibrary.simpleMessage("Ente\'de yeniyim"), "newest": MessageLookupByLibrary.simpleMessage("En yeni"), @@ -1257,7 +1356,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aygıt bulunamadı"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Yok"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Bu cihazda silinebilecek hiçbir dosyanız yok"), + "Her şey zaten temiz, silinecek dosya kalmadı"), "noDuplicates": MessageLookupByLibrary.simpleMessage("Yinelenenleri kaldır"), "noEnteAccountExclamation": @@ -1277,27 +1376,33 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Burada fotoğraf bulunamadı"), "noQuickLinksSelected": MessageLookupByLibrary.simpleMessage("Hızlı bağlantılar seçilmedi"), - "noRecoveryKey": - MessageLookupByLibrary.simpleMessage("Kurtarma kodunuz yok mu?"), + "noRecoveryKey": MessageLookupByLibrary.simpleMessage( + "Kurtarma anahtarınız yok mu?"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( "Uçtan uca şifreleme protokolümüzün doğası gereği, verileriniz şifreniz veya kurtarma anahtarınız olmadan çözülemez"), "noResults": MessageLookupByLibrary.simpleMessage("Sonuç bulunamadı"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Hiçbir sonuç bulunamadı"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Sistem kilidi bulunamadı"), - "notPersonLabel": m52, + "notPersonLabel": m53, + "notThisPerson": + MessageLookupByLibrary.simpleMessage("Bu kişi değil mi?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Henüz sizinle paylaşılan bir şey yok"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( "Burada görülecek bir şey yok! 👀"), "notifications": MessageLookupByLibrary.simpleMessage("Bildirimler"), "ok": MessageLookupByLibrary.simpleMessage("Tamam"), - "onDevice": MessageLookupByLibrary.simpleMessage("Bu cihaz"), + "onDevice": MessageLookupByLibrary.simpleMessage("Cihazda"), "onEnte": MessageLookupByLibrary.simpleMessage( "ente üzerinde"), - "onlyFamilyAdminCanChangeCode": m53, + "onTheRoad": MessageLookupByLibrary.simpleMessage("Yeniden yollarda"), + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Sadece onlar"), "oops": MessageLookupByLibrary.simpleMessage("Hay aksi"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1312,7 +1417,7 @@ class MessageLookup extends MessageLookupByLibrary { "openSettings": MessageLookupByLibrary.simpleMessage("Ayarları Açın"), "openTheItem": MessageLookupByLibrary.simpleMessage("• Öğeyi açın"), "openstreetmapContributors": MessageLookupByLibrary.simpleMessage( - "© OpenStreetMap katkıda bululanlar"), + "OpenStreetMap katkıda bululanlar"), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( "İsteğe bağlı, istediğiniz kadar kısa..."), "orMergeWithExistingPerson": MessageLookupByLibrary.simpleMessage( @@ -1327,16 +1432,17 @@ class MessageLookup extends MessageLookupByLibrary { "pairingComplete": MessageLookupByLibrary.simpleMessage("Eşleştirme tamamlandı"), "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("Doğrulama hala bekliyor"), - "passkey": MessageLookupByLibrary.simpleMessage("Parola Anahtarı"), + "passkey": MessageLookupByLibrary.simpleMessage("Geçiş anahtarı"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Geçiş anahtarı doğrulaması"), "password": MessageLookupByLibrary.simpleMessage("Şifre"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Şifreniz başarılı bir şekilde değiştirildi"), - "passwordLock": MessageLookupByLibrary.simpleMessage("Sifre kilidi"), - "passwordStrength": m55, + "passwordLock": MessageLookupByLibrary.simpleMessage("Şifre kilidi"), + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Parola gücü, parolanın uzunluğu, kullanılan karakterler ve parolanın en çok kullanılan ilk 10.000 parola arasında yer alıp almadığı dikkate alınarak hesaplanır"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1347,10 +1453,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ödeme başarısız oldu"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Maalesef ödemeniz başarısız oldu. Lütfen destekle iletişime geçin, size yardımcı olacağız!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Bekleyen Öğeler"), "pendingSync": - MessageLookupByLibrary.simpleMessage("Senkronizasyon bekleniyor"), + MessageLookupByLibrary.simpleMessage("Bekleyen Senkronizasyonlar"), "people": MessageLookupByLibrary.simpleMessage("Kişiler"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("Kodunuzu kullananlar"), @@ -1360,17 +1466,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kalıcı olarak sil"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Cihazdan kalıcı olarak silinsin mi?"), + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("Kişi Adı"), + "personTurningAge": m59, + "pets": MessageLookupByLibrary.simpleMessage("Tüylü dostlar"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Fotoğraf Açıklaması"), - "photoGridSize": - MessageLookupByLibrary.simpleMessage("Fotoğraf ızgara boyutu"), + "photoGridSize": MessageLookupByLibrary.simpleMessage("Izgara boyutu"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("fotoğraf"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("Fotoğraflar"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Eklediğiniz fotoğraflar albümden kaldırılacak"), + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage( "Fotoğraflar göreli zaman farkını korur"), @@ -1380,7 +1489,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Pin kilidi"), "playOnTv": MessageLookupByLibrary.simpleMessage("Albümü TV\'de oynat"), "playOriginal": MessageLookupByLibrary.simpleMessage("Orijinali oynat"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("Akışı oynat"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore aboneliği"), @@ -1393,14 +1502,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bu hata devam ederse lütfen desteğe başvurun"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Lütfen izin ver"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar giriş yapın"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Lütfen kaldırmak için hızlı bağlantıları seçin"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar deneyiniz"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1415,8 +1524,9 @@ class MessageLookup extends MessageLookupByLibrary { "Tekrar denemeden önce lütfen bir süre bekleyin"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( "Lütfen bekleyin, bu biraz zaman alabilir."), + "posingWithThem": m65, "preparingLogs": - MessageLookupByLibrary.simpleMessage("Günlük hazırlanıyor..."), + MessageLookupByLibrary.simpleMessage("Kayıtlar hazırlanıyor..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Daha fazlasını koruyun"), "pressAndHoldToPlayVideo": MessageLookupByLibrary.simpleMessage( @@ -1433,11 +1543,11 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Devam edin"), "processed": MessageLookupByLibrary.simpleMessage("İşlenen"), "processing": MessageLookupByLibrary.simpleMessage("İşleniyor"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("Videolar işleniyor"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage( - "Herkese açık link oluşturuldu"), + "Herkese açık bağlantı oluşturuldu"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( "Herkese açık bağlantı aktive edildi"), "queued": MessageLookupByLibrary.simpleMessage("Kuyrukta"), @@ -1445,14 +1555,16 @@ class MessageLookup extends MessageLookupByLibrary { "radius": MessageLookupByLibrary.simpleMessage("Yarıçap"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Bileti artır"), "rateTheApp": - MessageLookupByLibrary.simpleMessage("Uygulamaya puan verin"), + MessageLookupByLibrary.simpleMessage("Uygulamayı puanlayın"), "rateUs": MessageLookupByLibrary.simpleMessage("Bizi değerlendirin"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": - MessageLookupByLibrary.simpleMessage("\"Ben \"i yeniden atayın"), - "reassignedToName": m67, + MessageLookupByLibrary.simpleMessage("\"Ben\"i yeniden atayın"), + "reassignedToName": m68, "reassigningLoading": - MessageLookupByLibrary.simpleMessage("Yeniden atama..."), + MessageLookupByLibrary.simpleMessage("Yeniden atanıyor..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Kurtarma"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Hesabı kurtar"), "recoverButton": MessageLookupByLibrary.simpleMessage("Kurtar"), @@ -1460,7 +1572,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hesabı kurtar"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Kurtarma başlatıldı"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Kurtarma anahtarı"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1475,12 +1587,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kurtarma kodu doğrulandı"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarınız, şifrenizi unutmanız durumunda fotoğraflarınızı kurtarmanın tek yoludur. Kurtarma anahtarınızı Ayarlar > Hesap bölümünde bulabilirsiniz.\n\nDoğru kaydettiğinizi doğrulamak için lütfen kurtarma anahtarınızı buraya girin."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Kurtarma başarılı!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Güvenilir bir kişi hesabınıza erişmeye çalışıyor"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Cihazınız, şifrenizi doğrulamak için yeterli güce sahip değil, ancak tüm cihazlarda çalışacak şekilde yeniden oluşturabiliriz.\n\nLütfen kurtarma anahtarınızı kullanarak giriş yapın ve şifrenizi yeniden oluşturun (istediğiniz takdirde aynı şifreyi tekrar kullanabilirsiniz)."), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage( @@ -1496,26 +1608,26 @@ class MessageLookup extends MessageLookupByLibrary { "1. Bu kodu arkadaşlarınıza verin"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ücretli bir plan için kaydolsunlar"), - "referralStep3": m71, - "referrals": MessageLookupByLibrary.simpleMessage("Referanslar"), + "referralStep3": m72, + "referrals": + MessageLookupByLibrary.simpleMessage("Arkadaşını davet et"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Davetler şu anda durmuş durumda"), "rejectRecovery": MessageLookupByLibrary.simpleMessage("Kurtarmayı reddet"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( - "Ayrıca boşalan alanı talep etmek için \"Ayarlar\" -> \"Depolama\" bölümünden \"Son Silinenler \"i boşaltın"), + "Ayrıca boş alanı kazanmak için \"Ayarlar\" > \"Depolama\" bölümünden \"Son Silinenler\" klasörünü de boşaltın"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( "Ayrıca boşalan alana sahip olmak için \"Çöp Kutunuzu\" boşaltın"), - "remoteImages": - MessageLookupByLibrary.simpleMessage("Uzaktan Görüntüler"), + "remoteImages": MessageLookupByLibrary.simpleMessage("Uzak Görseller"), "remoteThumbnails": - MessageLookupByLibrary.simpleMessage("Uzak Küçük Resim"), - "remoteVideos": MessageLookupByLibrary.simpleMessage("Uzak videolar"), + MessageLookupByLibrary.simpleMessage("Uzak Küçük Resimler"), + "remoteVideos": MessageLookupByLibrary.simpleMessage("Uzak Videolar"), "remove": MessageLookupByLibrary.simpleMessage("Kaldır"), "removeDuplicates": MessageLookupByLibrary.simpleMessage("Yinelenenleri kaldır"), "removeDuplicatesDesc": MessageLookupByLibrary.simpleMessage( - "Tam olarak yinelenen dosyaları gözden geçirin ve kaldırın."), + "Aynı olan dosyaları gözden geçirin ve kaldırın."), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("Albümden çıkar"), "removeFromAlbumTitle": @@ -1524,20 +1636,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Favorilerden Kaldır"), "removeInvite": MessageLookupByLibrary.simpleMessage("Davetiyeyi kaldır"), - "removeLink": MessageLookupByLibrary.simpleMessage("Linki kaldır"), + "removeLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı kaldır"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Katılımcıyı kaldır"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Kişi etiketini kaldırın"), - "removePublicLink": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), - "removePublicLinks": - MessageLookupByLibrary.simpleMessage("Herkese açık link oluştur"), + "removePublicLink": MessageLookupByLibrary.simpleMessage( + "Herkese açık bağlantıyı kaldır"), + "removePublicLinks": MessageLookupByLibrary.simpleMessage( + "Herkese açık bağlantıları kaldır"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "Kaldırdığınız öğelerden bazıları başkaları tarafından eklenmiştir ve bunlara erişiminizi kaybedeceksiniz"), "removeWithQuestionMark": - MessageLookupByLibrary.simpleMessage("Kaldır?"), + MessageLookupByLibrary.simpleMessage("Kaldırılsın mı?"), "removeYourselfAsTrustedContact": MessageLookupByLibrary.simpleMessage( "Kendinizi güvenilir kişi olarak kaldırın"), "removingFromFavorites": @@ -1549,8 +1661,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dosyayı yeniden adlandır"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonelik yenileme"), - "renewsOn": m73, - "reportABug": MessageLookupByLibrary.simpleMessage("Hatayı bildir"), + "renewsOn": m74, + "reportABug": MessageLookupByLibrary.simpleMessage("Hata bildir"), "reportBug": MessageLookupByLibrary.simpleMessage("Hata bildir"), "resendEmail": MessageLookupByLibrary.simpleMessage("E-postayı yeniden gönder"), @@ -1561,7 +1673,7 @@ class MessageLookup extends MessageLookupByLibrary { "resetPerson": MessageLookupByLibrary.simpleMessage("Kaldır"), "resetToDefault": MessageLookupByLibrary.simpleMessage("Varsayılana sıfırla"), - "restore": MessageLookupByLibrary.simpleMessage("Yenile"), + "restore": MessageLookupByLibrary.simpleMessage("Geri yükle"), "restoreToAlbum": MessageLookupByLibrary.simpleMessage("Albümü yenile"), "restoringFiles": MessageLookupByLibrary.simpleMessage("Dosyalar geri yükleniyor..."), @@ -1574,6 +1686,7 @@ class MessageLookup extends MessageLookupByLibrary { "reviewSuggestions": MessageLookupByLibrary.simpleMessage("Önerileri inceleyin"), "right": MessageLookupByLibrary.simpleMessage("Sağ"), + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("Döndür"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Sola döndür"), "rotateRight": MessageLookupByLibrary.simpleMessage("Sağa döndür"), @@ -1582,11 +1695,11 @@ class MessageLookup extends MessageLookupByLibrary { "save": MessageLookupByLibrary.simpleMessage("Kaydet"), "saveChangesBeforeLeavingQuestion": MessageLookupByLibrary.simpleMessage( - "Ayrılmadan önce değişiklikleri kaydedin mi?"), + "Çıkmadan önce değişiklikler kaydedilsin mi?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Kolajı kaydet"), "saveCopy": MessageLookupByLibrary.simpleMessage("Kopyasını kaydet"), "saveKey": MessageLookupByLibrary.simpleMessage("Anahtarı kaydet"), - "savePerson": MessageLookupByLibrary.simpleMessage("Kişiyi Kaydet"), + "savePerson": MessageLookupByLibrary.simpleMessage("Kişiyi kaydet"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( "Henüz yapmadıysanız kurtarma anahtarınızı kaydetmeyi unutmayın"), @@ -1611,7 +1724,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchDiscoverEmptySection": MessageLookupByLibrary.simpleMessage( "İşleme ve senkronizasyon tamamlandığında görüntüler burada gösterilecektir"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( - "İndeksleme yapıldıktan sonra insanlar burada gösterilecek"), + "Dizinleme yapıldıktan sonra insanlar burada gösterilecek"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Dosya türleri ve adları"), "searchHint1": @@ -1629,8 +1742,8 @@ class MessageLookup extends MessageLookupByLibrary { "İnsanları davet ettiğinizde onların paylaştığı tüm fotoğrafları burada göreceksiniz"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "İşleme ve senkronizasyon tamamlandığında kişiler burada gösterilecektir"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Güvenlik"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Uygulamadaki herkese açık albüm bağlantılarını görün"), @@ -1676,12 +1789,16 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Seçilen öğeler tüm albümlerden silinecek ve çöp kutusuna taşınacak."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedItemsWillBeRemovedFromThisPerson": + MessageLookupByLibrary.simpleMessage( + "Seçili öğeler bu kişiden silinir, ancak kitaplığınızdan silinmez."), + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("Gönder"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-posta gönder"), "sendInvite": MessageLookupByLibrary.simpleMessage("Davet kodu gönder"), - "sendLink": MessageLookupByLibrary.simpleMessage("Link gönder"), + "sendLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı gönder"), "serverEndpoint": MessageLookupByLibrary.simpleMessage("Sunucu uç noktası"), "sessionExpired": @@ -1702,22 +1819,23 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Ayarlama işlemi başarılı"), "share": MessageLookupByLibrary.simpleMessage("Paylaş"), - "shareALink": MessageLookupByLibrary.simpleMessage("Linki paylaş"), + "shareALink": + MessageLookupByLibrary.simpleMessage("Bir bağlantı paylaş"), "shareAlbumHint": MessageLookupByLibrary.simpleMessage( "Bir albüm açın ve paylaşmak için sağ üstteki paylaş düğmesine dokunun."), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Şimdi bir albüm paylaşın"), - "shareLink": MessageLookupByLibrary.simpleMessage("Linki paylaş"), - "shareMyVerificationID": m79, + "shareLink": MessageLookupByLibrary.simpleMessage("Bağlantıyı paylaş"), + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Yalnızca istediğiniz kişilerle paylaşın"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Orijinal kalitede fotoğraf ve videoları kolayca paylaşabilmemiz için Ente\'yi indirin\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Ente kullanıcısı olmayanlar için paylaş"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("İlk albümünüzü paylaşın"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1728,8 +1846,8 @@ class MessageLookup extends MessageLookupByLibrary { "sharedPhotoNotifications": MessageLookupByLibrary.simpleMessage( "Paylaşılan fotoğrafları ekle"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( - "Birisi sizin de parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın"), - "sharedWith": m83, + "Birisi parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın"), + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Benimle paylaşılan"), "sharedWithYou": @@ -1747,11 +1865,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Diğer cihazlardan çıkış yap"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Hizmet Şartları\'nı ve Gizlilik Politikası\'nı kabul ediyorum"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("Tüm albümlerden silinecek."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Geç"), "social": MessageLookupByLibrary.simpleMessage("Sosyal Medya"), "someItemsAreInBothEnteAndYourDevice": @@ -1769,6 +1887,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Bir şeyler ters gitti, lütfen tekrar deneyin"), "sorry": MessageLookupByLibrary.simpleMessage("Üzgünüz"), + "sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage( + "Üzgünüz, bu dosya şu anda yedeklenemedi. Daha sonra tekrar deneyeceğiz."), "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( "Üzgünüm, favorilere ekleyemedim!"), "sorryCouldNotRemoveFromFavorites": @@ -1780,12 +1900,18 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Üzgünüm, bu cihazda güvenli anahtarlarını oluşturamadık.\n\nLütfen başka bir cihazdan giriş yapmayı deneyiniz."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sırala"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sırala"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Yeniden eskiye"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Önce en eski"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Başarılı"), + "sportsWithThem": m89, + "spotlightOnThem": m90, + "spotlightOnYourself": + MessageLookupByLibrary.simpleMessage("Sahne senin"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("Kurtarmayı başlat"), "startBackup": @@ -1798,15 +1924,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Depolama"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Aile"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sen"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Depolama sınırı aşıldı"), - "storageUsageInfo": m90, - "streamDetails": - MessageLookupByLibrary.simpleMessage("Yayın detayları"), + "storageUsageInfo": m92, + "streamDetails": MessageLookupByLibrary.simpleMessage("Akış detayları"), "strongStrength": MessageLookupByLibrary.simpleMessage("Güçlü"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Abone ol"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Paylaşımı etkinleştirmek için aktif bir ücretli aboneliğe ihtiyacınız var."), @@ -1822,8 +1947,9 @@ class MessageLookup extends MessageLookupByLibrary { "Başarıyla arşivden çıkarıldı"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("Özellik önerin"), + "sunrise": MessageLookupByLibrary.simpleMessage("Ufukta"), "support": MessageLookupByLibrary.simpleMessage("Destek"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Senkronizasyon durduruldu"), "syncing": MessageLookupByLibrary.simpleMessage("Eşitleniyor..."), @@ -1835,12 +1961,12 @@ class MessageLookup extends MessageLookupByLibrary { "tapToUnlock": MessageLookupByLibrary.simpleMessage("Açmak için dokun"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Yüklemek için tıklayın"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin."), "terminate": MessageLookupByLibrary.simpleMessage("Sonlandır"), "terminateSession": - MessageLookupByLibrary.simpleMessage("Oturumu sonlandır?"), + MessageLookupByLibrary.simpleMessage("Oturum sonlandırılsın mı?"), "terms": MessageLookupByLibrary.simpleMessage("Şartlar"), "termsOfServicesTitle": MessageLookupByLibrary.simpleMessage("Şartlar"), "thankYou": MessageLookupByLibrary.simpleMessage("Teşekkürler"), @@ -1858,7 +1984,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Bu öğeler cihazınızdan silinecektir."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("Tüm albümlerden silinecek."), "thisActionCannotBeUndone": @@ -1876,9 +2002,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bu görselde exif verisi yok"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Bu benim!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Doğrulama kimliğiniz"), + "thisWeekThroughTheYears": + MessageLookupByLibrary.simpleMessage("Yıllar boyunca bu hafta"), + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage( "Bu, sizi aşağıdaki cihazdan çıkış yapacak:"), @@ -1890,6 +2019,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage( "Bu, seçilen tüm hızlı bağlantıların genel bağlantılarını kaldıracaktır."), + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( "Uygulama kilidini etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın."), @@ -1897,25 +2027,27 @@ class MessageLookup extends MessageLookupByLibrary { "Bir fotoğrafı veya videoyu gizlemek için"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( "Şifrenizi sıfılamak için lütfen e-postanızı girin."), - "todaysLogs": - MessageLookupByLibrary.simpleMessage("Bugünün günlükleri"), + "todaysLogs": MessageLookupByLibrary.simpleMessage("Bugünün kayıtları"), "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage("Çok fazla hatalı deneme"), "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Toplam boyut"), "trash": MessageLookupByLibrary.simpleMessage("Cöp kutusu"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Kes"), + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("Güvenilir kişiler"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Tekrar deneyiniz"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Bu cihaz klasörüne eklenen dosyaları otomatik olarak ente\'ye yüklemek için yedeklemeyi açın."), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "Yıllık planlarda 2 ay ücretsiz"), - "twofactor": MessageLookupByLibrary.simpleMessage("İki faktör"), + "twofactor": + MessageLookupByLibrary.simpleMessage("İki faktörlü doğrulama"), "twofactorAuthenticationHasBeenDisabled": MessageLookupByLibrary.simpleMessage( "İki faktörlü kimlik doğrulama devre dışı"), @@ -1925,8 +2057,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "İki faktörlü kimlik doğrulama başarıyla sıfırlandı"), "twofactorSetup": - MessageLookupByLibrary.simpleMessage("Cift faktör ayarı"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + MessageLookupByLibrary.simpleMessage("İki faktörlü kurulum"), + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Arşivden cıkar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Arşivden Çıkar"), @@ -1951,10 +2083,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Klasör seçimi güncelleniyor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Yükselt"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dosyalar albüme taşınıyor..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("1 anı korunuyor..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1973,7 +2105,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Seçilen fotoğrafı kullan"), "usedSpace": MessageLookupByLibrary.simpleMessage("Kullanılan alan"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Doğrulama başarısız oldu, lütfen tekrar deneyin"), @@ -1982,7 +2114,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-posta adresini doğrulayın"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Şifrenizi doğrulayın"), @@ -1993,7 +2125,8 @@ class MessageLookup extends MessageLookupByLibrary { "Kurtarma kodu doğrulanıyor..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Video Bilgileri"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("Video akışı"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Akışlandırılabilir videolar"), "videos": MessageLookupByLibrary.simpleMessage("Videolar"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktif oturumları görüntüle"), @@ -2005,11 +2138,13 @@ class MessageLookup extends MessageLookupByLibrary { "viewLargeFiles": MessageLookupByLibrary.simpleMessage("Büyük dosyalar"), "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( - "En fazla depolama alanı tüketen dosyaları görüntüleyin."), - "viewLogs": MessageLookupByLibrary.simpleMessage("Günlükleri göster"), + "En fazla depolama alanı kullanan dosyaları görüntüleyin."), + "viewLogs": MessageLookupByLibrary.simpleMessage("Kayıtları görüntüle"), + "viewPersonToUnlink": m110, "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarını görüntüle"), "viewer": MessageLookupByLibrary.simpleMessage("Görüntüleyici"), + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi yönetmek için lütfen web.ente.io adresini ziyaret edin"), "waitingForVerification": @@ -2022,7 +2157,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Henüz sahibi olmadığınız fotoğraf ve albümlerin düzenlenmesini desteklemiyoruz"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Zayıf"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Tekrardan hoşgeldin!"), @@ -2030,7 +2165,7 @@ class MessageLookup extends MessageLookupByLibrary { "whyAddTrustContact": MessageLookupByLibrary.simpleMessage("."), "yearShort": MessageLookupByLibrary.simpleMessage("yıl"), "yearly": MessageLookupByLibrary.simpleMessage("Yıllık"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Evet"), "yesCancel": MessageLookupByLibrary.simpleMessage("Evet, iptal et"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2045,6 +2180,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesResetPerson": MessageLookupByLibrary.simpleMessage("Evet, kişiyi sıfırla"), "you": MessageLookupByLibrary.simpleMessage("Sen"), + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Aile planı kullanıyorsunuz!"), "youAreOnTheLatestVersion": @@ -2063,7 +2199,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kendinizle paylaşamazsınız"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Arşivlenmiş öğeniz yok."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Hesabınız silindi"), "yourMap": MessageLookupByLibrary.simpleMessage("Haritalarınız"), @@ -2071,7 +2207,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Planınız başarıyla düşürüldü"), "yourPlanWasSuccessfullyUpgraded": MessageLookupByLibrary.simpleMessage( - "Planınız başarılı şekilde yükseltildi"), + "Planınız başarıyla yükseltildi"), "yourPurchaseWasSuccessful": MessageLookupByLibrary.simpleMessage("Satın alım başarılı"), "yourStorageDetailsCouldNotBeFetched": @@ -2088,7 +2224,7 @@ class MessageLookup extends MessageLookupByLibrary { "Temizlenebilecek yinelenen dosyalarınız yok"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( - "Bu cihazda silinebilecek hiçbir dosyanız yok"), + "Her şey zaten temiz, silinecek dosya kalmadı"), "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage( "Fotoğrafları görmek için uzaklaştırın") }; diff --git a/mobile/lib/generated/intl/messages_uk.dart b/mobile/lib/generated/intl/messages_uk.dart index 73ed062ccb..aa14a02cd3 100644 --- a/mobile/lib/generated/intl/messages_uk.dart +++ b/mobile/lib/generated/intl/messages_uk.dart @@ -51,9 +51,6 @@ class MessageLookup extends MessageLookupByLibrary { static String m15(albumName) => "Створено спільне посилання для «${albumName}»"; - static String m16(count) => - "${Intl.plural(count, zero: 'Додано 0 співавторів', one: 'Додано 1 співавтор', few: 'Додано ${count} співаторів', many: 'Додано ${count} співаторів', other: 'Додано ${count} співавторів')}"; - static String m17(email, numOfDays) => "Ви збираєтеся додати ${email} як довірений контакт. Вони зможуть відновити ваш обліковий запис, якщо ви будете відсутні протягом ${numOfDays} днів."; @@ -66,7 +63,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m20(endpoint) => "Під\'єднано до ${endpoint}"; static String m21(count) => - "${Intl.plural(count, one: 'Видалено ${count} елемент', few: 'Видалено ${count} елементи', many: 'Видалено ${count} елементів', other: 'Видалено ${count} елементів')}"; + "${Intl.plural(count, one: 'Видалено ${count} елемент', other: 'Видалено ${count} елементів')}"; static String m22(currentlyDeleting, totalCount) => "Видалення ${currentlyDeleting} / ${totalCount}"; @@ -83,156 +80,156 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} файлів, кожен по ${formattedSize}"; - static String m27(newEmail) => "Поштову адресу змінено на ${newEmail}"; + static String m28(newEmail) => "Поштову адресу змінено на ${newEmail}"; - static String m29(email) => + static String m30(email) => "У ${email} немає облікового запису Ente.\n\nНадішліть їм запрошення для обміну фотографіями."; - static String m31(text) => "Знайдено додаткові фотографії для ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} на цьому пристрої було створено резервну копію"; + static String m32(text) => "Знайдено додаткові фотографії для ${text}"; static String m34(count, formattedNumber) => - "${Intl.plural(count, one: 'Для 1 файлу', few: 'Для ${formattedNumber} файлів', many: 'Для ${formattedNumber} файлів', other: 'Для ${formattedNumber} файлів')} у цьому альбомі було створено резервну копію"; + "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} на цьому пристрої було створено резервну копію"; - static String m35(storageAmountInGB) => + static String m35(count, formattedNumber) => + "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} у цьому альбомі було створено резервну копію"; + + static String m36(storageAmountInGB) => "${storageAmountInGB} ГБ щоразу, коли хтось оформлює передплату і застосовує ваш код"; - static String m36(endDate) => "Безплатна пробна версія діє до ${endDate}"; + static String m37(endDate) => "Безплатна пробна версія діє до ${endDate}"; - static String m38(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Обробка ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} елемент', few: '${count} елементи', many: '${count} елементів', other: '${count} елементів')}"; - static String m44(email) => "${email} запросив вас стати довіреною особою"; + static String m45(email) => "${email} запросив вас стати довіреною особою"; - static String m45(expiryTime) => "Посилання закінчується через ${expiryTime}"; + static String m46(expiryTime) => "Посилання закінчується через ${expiryTime}"; - static String m50(albumName) => "Успішно перенесено до «${albumName}»"; + static String m51(albumName) => "Успішно перенесено до «${albumName}»"; - static String m51(personName) => "Немає пропозицій для ${personName}"; + static String m52(personName) => "Немає пропозицій для ${personName}"; - static String m52(name) => "Не ${name}?"; + static String m53(name) => "Не ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Зв\'яжіться з ${familyAdminEmail}, щоб змінити код."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Надійність пароля: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Зверніться до ${providerName}, якщо було знято платіж"; - static String m61(endDate) => + static String m62(endDate) => "Безплатна пробна версія діє до ${endDate}.\nПісля цього ви можете обрати платний план."; - static String m62(toEmail) => "Напишіть нам на ${toEmail}"; + static String m63(toEmail) => "Напишіть нам на ${toEmail}"; - static String m63(toEmail) => "Надішліть журнали на \n${toEmail}"; + static String m64(toEmail) => "Надішліть журнали на \n${toEmail}"; - static String m65(folderName) => "Оброблюємо «${folderName}»..."; + static String m66(folderName) => "Оброблюємо «${folderName}»..."; - static String m66(storeName) => "Оцініть нас в ${storeName}"; + static String m67(storeName) => "Оцініть нас в ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Ви зможете отримати доступ до облікового запису через ${days} днів. Повідомлення буде надіслано на ${email}."; - static String m69(email) => + static String m70(email) => "Тепер ви можете відновити обліковий запис ${email}, встановивши новий пароль."; - static String m70(email) => + static String m71(email) => "${email} намагається відновити ваш обліковий запис."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Ви обоє отримуєте ${storageInGB} ГБ* безплатно"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} буде видалено з цього спільного альбому\n\nБудь-які додані вами фото, будуть також видалені з альбому"; - static String m73(endDate) => "Передплата поновиться ${endDate}"; + static String m74(endDate) => "Передплата поновиться ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: 'Знайдено ${count} результат', few: 'Знайдено ${count} результати', many: 'Знайдено ${count} результатів', other: 'Знайдено ${count} результати')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Невідповідність довжини розділів: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} вибрано"; + static String m78(count) => "${count} вибрано"; - static String m77(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; + static String m79(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; - static String m79(verificationID) => + static String m81(verificationID) => "Ось мій ідентифікатор підтвердження: ${verificationID} для ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Гей, ви можете підтвердити, що це ваш ідентифікатор підтвердження: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Реферальний код Ente: ${referralCode} \n\nЗастосуйте його в «Налаштування» → «Загальні» → «Реферали», щоб отримати ${referralStorageInGB} ГБ безплатно після переходу на платний тариф\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поділитися з конкретними людьми', one: 'Поділитися з 1 особою', other: 'Поділитися з ${numberOfPeople} людьми')}"; - static String m83(emailIDs) => "Поділилися з ${emailIDs}"; + static String m85(emailIDs) => "Поділилися з ${emailIDs}"; - static String m84(fileType) => "Цей ${fileType} буде видалено з пристрою."; + static String m86(fileType) => "Цей ${fileType} буде видалено з пристрою."; - static String m85(fileType) => + static String m87(fileType) => "Цей ${fileType} знаходиться і в Ente, і на вашому пристрої."; - static String m86(fileType) => "Цей ${fileType} буде видалено з Ente."; + static String m88(fileType) => "Цей ${fileType} буде видалено з Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} ГБ"; + static String m91(storageAmountInGB) => "${storageAmountInGB} ГБ"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} з ${totalAmount} ${totalStorageUnit} використано"; - static String m91(id) => + static String m93(id) => "Ваш ${id} вже пов\'язаний з іншим обліковим записом Ente.\nЯкщо ви хочете використовувати свій ${id} з цим обліковим записом, зверніться до нашої служби підтримки"; - static String m92(endDate) => "Вашу передплату буде скасовано ${endDate}"; + static String m94(endDate) => "Вашу передплату буде скасовано ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed} / ${total} спогадів збережено"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Натисніть, щоб завантажити; завантаження наразі ігнорується через: ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Вони також отримують ${storageAmountInGB} ГБ"; - static String m96(email) => "Це ідентифікатор підтвердження пошти ${email}"; + static String m98(email) => "Це ідентифікатор підтвердження пошти ${email}"; - static String m99(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Незабаром', one: '1 день', other: '${count} днів')}"; - static String m102(email) => + static String m104(email) => "Ви отримали запрошення стати спадковим контактом від ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Тип галереї «${galleryType}» не підтримується для перейменування"; - static String m104(ignoreReason) => + static String m106(ignoreReason) => "Завантаження проігноровано через: ${ignoreReason}"; - static String m105(count) => "Збереження ${count} спогадів..."; + static String m107(count) => "Збереження ${count} спогадів..."; - static String m106(endDate) => "Діє до ${endDate}"; + static String m108(endDate) => "Діє до ${endDate}"; - static String m107(email) => "Підтвердити ${email}"; + static String m109(email) => "Підтвердити ${email}"; - static String m109(email) => "Ми надіслали листа на ${email}"; + static String m112(email) => "Ми надіслали листа на ${email}"; - static String m110(count) => - "${Intl.plural(count, one: '${count} рік тому', few: '${count} роки тому', many: '${count} років тому', other: '${count} років тому')}"; + static String m113(count) => + "${Intl.plural(count, one: '${count} рік тому', other: '${count} років тому')}"; - static String m112(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; + static String m115(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -449,6 +446,7 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("Резервне копіювання відео"), "birthday": MessageLookupByLibrary.simpleMessage("День народження"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage( "Розпродаж у «Чорну п\'ятницю»"), "blog": MessageLookupByLibrary.simpleMessage("Блог"), @@ -517,6 +515,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Натисніть"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( "• Натисніть на меню переповнення"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Закрити"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Клуб за часом захоплення"), @@ -541,7 +541,6 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Співавтори можуть додавати фотографії та відео до спільного альбому."), - "collaboratorsSuccessfullyAdded": m16, "collageLayout": MessageLookupByLibrary.simpleMessage("Макет"), "collageSaved": MessageLookupByLibrary.simpleMessage("Колаж збережено до галереї"), @@ -762,8 +761,8 @@ class MessageLookup extends MessageLookupByLibrary { "eligible": MessageLookupByLibrary.simpleMessage("придатний"), "email": MessageLookupByLibrary.simpleMessage("Адреса електронної пошти"), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Підтвердження через пошту"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -845,7 +844,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Експортувати дані"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Знайдено додаткові фотографії"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Обличчя ще не згруповані, поверніться пізніше"), "faceRecognition": @@ -895,8 +894,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типи файлів"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типи та назви файлів"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файли видалено"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файли збережено до галереї"), @@ -912,13 +911,13 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Знайдені обличчя"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Безплатне сховище отримано"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Безплатне сховище можна використовувати"), "freeTrial": MessageLookupByLibrary.simpleMessage("Безплатний пробний період"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Звільніть місце на пристрої"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -929,7 +928,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Загальні"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Створення ключів шифрування..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти до налаштувань"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -942,6 +941,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Гостьовий перегляд"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Щоб увімкнути гостьовий перегляд, встановіть пароль або блокування екрана в налаштуваннях системи."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Ми не відстежуємо встановлення застосунку. Але, якщо ви скажете нам, де ви нас знайшли, це допоможе!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1010,7 +1011,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Елементи показують кількість днів, що залишилися до остаточного видалення"), @@ -1034,7 +1035,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Спадок"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Облікові записи «Спадку»"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "«Спадок» дозволяє довіреним контактам отримати доступ до вашого облікового запису під час вашої відсутності."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1047,7 +1048,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Досягнуто ліміту пристроїв"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Увімкнено"), "linkExpired": MessageLookupByLibrary.simpleMessage("Закінчився"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage( "Термін дії посилання закінчився"), "linkHasExpired": @@ -1056,8 +1057,6 @@ class MessageLookup extends MessageLookupByLibrary { "livePhotos": MessageLookupByLibrary.simpleMessage("Живі фото"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Ви можете поділитися своєю передплатою з родиною"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "На цей час ми зберегли понад 30 мільйонів спогадів"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Ми зберігаємо 3 копії ваших даних, одну в підземному бункері"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1115,6 +1114,8 @@ class MessageLookup extends MessageLookupByLibrary { "Довго утримуйте поштову адресу, щоб перевірити наскрізне шифрування."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Натисніть і утримуйте елемент для перегляду в повноекранному режимі"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Вимкнено зациклювання відео"), "loopVideoOn": MessageLookupByLibrary.simpleMessage( @@ -1177,7 +1178,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Перемістити до альбому"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Перемістити до прихованого альбому"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Переміщено у смітник"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1193,6 +1194,7 @@ class MessageLookup extends MessageLookupByLibrary { "newLocation": MessageLookupByLibrary.simpleMessage("Нове розташування"), "newPerson": MessageLookupByLibrary.simpleMessage("Нова особа"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Уперше на Ente"), "newest": MessageLookupByLibrary.simpleMessage("Найновіші"), "next": MessageLookupByLibrary.simpleMessage("Далі"), @@ -1229,10 +1231,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Немає результатів"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Нічого не знайдено"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Не знайдено системного блокування"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Поки що з вами ніхто не поділився"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1242,7 +1244,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("На пристрої"), "onEnte": MessageLookupByLibrary.simpleMessage("В Ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Тільки вони"), "oops": MessageLookupByLibrary.simpleMessage("От халепа"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1282,7 +1287,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Пароль успішно змінено"), "passwordLock": MessageLookupByLibrary.simpleMessage("Блокування паролем"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Надійність пароля розраховується з урахуванням довжини пароля, використаних символів, а також того, чи входить пароль у топ 10 000 найбільш використовуваних паролів"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1293,7 +1298,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Не вдалося оплатити"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "На жаль, ваш платіж не вдався. Зв\'яжіться зі службою підтримки і ми вам допоможемо!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Елементи на розгляді"), "pendingSync": @@ -1323,7 +1328,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("Блокування PIN-кодом"), "playOnTv": MessageLookupByLibrary.simpleMessage("Відтворити альбом на ТБ"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Передплата Play Store"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1335,14 +1340,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Зверніться до служби підтримки, якщо проблема не зникне"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Надайте дозволи"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Увійдіть знову"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Виберіть посилання для видалення"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Спробуйте ще раз"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1369,7 +1374,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Приватне поширення"), "proceed": MessageLookupByLibrary.simpleMessage("Продовжити"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Публічне посилання створено"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1380,7 +1385,9 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оцініть застосунок"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцініть нас"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Відновити"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), @@ -1389,7 +1396,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Почато відновлення"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Ключ відновлення"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Ключ відновлення скопійовано в буфер обміну"), @@ -1403,12 +1410,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ключ відновлення перевірено"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Ключ відновлення — це єдиний спосіб відновити фотографії, якщо ви забули пароль. Ви можете знайти свій ключ в розділі «Налаштування» > «Обліковий запис».\n\nВведіть ключ відновлення тут, щоб перевірити, чи правильно ви його зберегли."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Відновлення успішне!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Довірений контакт намагається отримати доступ до вашого облікового запису"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Ваш пристрій недостатньо потужний для перевірки пароля, але ми можемо відновити його таким чином, щоб він працював на всіх пристроях.\n\nУвійдіть за допомогою ключа відновлення та відновіть свій пароль (за бажанням ви можете використати той самий ключ знову)."), "recreatePasswordTitle": @@ -1424,7 +1431,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("1. Дайте цей код друзям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Вони оформлюють передплату"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Реферали"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Реферали зараз призупинені"), @@ -1456,7 +1463,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вилучити посилання"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Видалити учасника"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Видалити мітку особи"), "removePublicLink": @@ -1478,7 +1485,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Перейменувати файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Поновити передплату"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Повідомити про помилку"), "reportBug": @@ -1558,8 +1565,8 @@ class MessageLookup extends MessageLookupByLibrary { "Запросіть людей, і ви побачите всі фотографії, якими вони поділилися, тут"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Люди будуть показані тут після завершення оброблення та синхронізації"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Безпека"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Посилання на публічні альбоми в застосунку"), @@ -1591,8 +1598,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Вибрані елементи будуть видалені з усіх альбомів і переміщені в смітник."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Надіслати"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Надіслати електронного листа"), @@ -1629,16 +1636,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Поділитися альбомом зараз"), "shareLink": MessageLookupByLibrary.simpleMessage("Поділитися посиланням"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Поділіться тільки з тими людьми, якими ви хочете"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Завантажте Ente для того, щоб легко поділитися фотографіями оригінальної якості та відео\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поділитися з користувачами без Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Поділитися вашим першим альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1649,7 +1656,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Нові спільні фотографії"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Отримувати сповіщення, коли хтось додасть фото до спільного альбому, в якому ви перебуваєте"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Поділитися зі мною"), "sharedWithYou": @@ -1666,11 +1673,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вийти на інших пристроях"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я приймаю умови використання і політику приватності"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Воно буде видалено з усіх альбомів."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Пропустити"), "social": MessageLookupByLibrary.simpleMessage("Соцмережі"), "someItemsAreInBothEnteAndYourDevice": @@ -1699,6 +1706,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "На жаль, на цьому пристрої не вдалося створити безпечні ключі.\n\nЗареєструйтесь з іншого пристрою."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Сортувати"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Сортувати за"), "sortNewestFirst": @@ -1718,13 +1727,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Сховище"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Сім\'я"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Ви"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Перевищено ліміт сховища"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Надійний"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Передплачувати"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Вам потрібна активна передплата, щоб увімкнути спільне поширення."), @@ -1741,7 +1750,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Запропонувати нові функції"), "support": MessageLookupByLibrary.simpleMessage("Підтримка"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронізацію зупинено"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронізуємо..."), @@ -1754,7 +1763,7 @@ class MessageLookup extends MessageLookupByLibrary { "Торкніться, щоби розблокувати"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Натисніть, щоб завантажити"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), "terminate": MessageLookupByLibrary.simpleMessage("Припинити"), @@ -1777,7 +1786,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ці елементи будуть видалені з пристрою."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Вони будуть видалені з усіх альбомів."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1793,7 +1802,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ця поштова адреса вже використовується"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Це зображення не має даних exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Це ваш Ідентифікатор підтвердження"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1818,11 +1827,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("всього"), "totalSize": MessageLookupByLibrary.simpleMessage("Загальний розмір"), "trash": MessageLookupByLibrary.simpleMessage("Смітник"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Вирізати"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Довірені контакти"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Спробувати знову"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Увімкніть резервну копію для автоматичного завантаження файлів, доданих до теки пристрою в Ente."), @@ -1840,7 +1849,7 @@ class MessageLookup extends MessageLookupByLibrary { "Двоетапну перевірку успішно скинуто"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Налаштування двоетапної перевірки"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Розархівувати"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Розархівувати альбом"), @@ -1863,10 +1872,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Оновлення вибору теки..."), "upgrade": MessageLookupByLibrary.simpleMessage("Покращити"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Завантажуємо файли до альбому..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Зберігаємо 1 спогад..."), "upto50OffUntil4thDec": @@ -1885,7 +1894,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Використати вибране фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Використано місця"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Перевірка не вдалася, спробуйте ще раз"), @@ -1894,7 +1903,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Підтвердити"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Підтвердити пошту"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Підтвердження"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Підтвердити ключ доступу"), @@ -1933,7 +1942,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Ми не підтримуємо редагування фотографій та альбомів, якими ви ще не володієте"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Слабкий"), "welcomeBack": MessageLookupByLibrary.simpleMessage("З поверненням!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Що нового"), @@ -1941,7 +1950,7 @@ class MessageLookup extends MessageLookupByLibrary { "Довірений контакт може допомогти у відновленні ваших даних."), "yearShort": MessageLookupByLibrary.simpleMessage("рік"), "yearly": MessageLookupByLibrary.simpleMessage("Щороку"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Так"), "yesCancel": MessageLookupByLibrary.simpleMessage("Так, скасувати"), "yesConvertToViewer": @@ -1974,7 +1983,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ви не можете поділитися із собою"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас немає жодних архівних елементів."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Ваш обліковий запис видалено"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша мапа"), diff --git a/mobile/lib/generated/intl/messages_vi.dart b/mobile/lib/generated/intl/messages_vi.dart index 8b0c479ba2..0c4f4001c9 100644 --- a/mobile/lib/generated/intl/messages_vi.dart +++ b/mobile/lib/generated/intl/messages_vi.dart @@ -84,160 +84,160 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} tệp, ${formattedSize} mỗi tệp"; - static String m27(newEmail) => "Email đã được thay đổi thành ${newEmail}"; + static String m28(newEmail) => "Email đã được thay đổi thành ${newEmail}"; - static String m29(email) => + static String m30(email) => "${email} không có tài khoản Ente.\n\nGửi cho họ một lời mời để chia sẻ ảnh."; - static String m31(text) => "Extra photos found for ${text}"; - - static String m33(count, formattedNumber) => - "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trên thiết bị này đã được sao lưu an toàn"; + static String m32(text) => "Extra photos found for ${text}"; static String m34(count, formattedNumber) => + "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trên thiết bị này đã được sao lưu an toàn"; + + static String m35(count, formattedNumber) => "${Intl.plural(count, one: '1 tệp', other: '${formattedNumber} tệp')} trong album này đã được sao lưu an toàn"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "${storageAmountInGB} GB mỗi khi ai đó đăng ký gói trả phí và áp dụng mã của bạn"; - static String m36(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; + static String m37(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; - static String m38(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "Đang xử lý ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} mục', other: '${count} mục')}"; - static String m44(email) => + static String m45(email) => "${email} đã mời bạn trở thành một liên hệ tin cậy"; - static String m45(expiryTime) => "Liên kết sẽ hết hạn vào ${expiryTime}"; + static String m46(expiryTime) => "Liên kết sẽ hết hạn vào ${expiryTime}"; - static String m50(albumName) => "Đã di chuyển thành công đến ${albumName}"; + static String m51(albumName) => "Đã di chuyển thành công đến ${albumName}"; - static String m51(personName) => "Không có gợi ý cho ${personName}"; + static String m52(personName) => "Không có gợi ý cho ${personName}"; - static String m52(name) => "Không phải ${name}?"; + static String m53(name) => "Không phải ${name}?"; - static String m53(familyAdminEmail) => + static String m54(familyAdminEmail) => "Vui lòng liên hệ ${familyAdminEmail} để thay đổi mã của bạn."; - static String m55(passwordStrengthValue) => + static String m56(passwordStrengthValue) => "Độ mạnh mật khẩu: ${passwordStrengthValue}"; - static String m56(providerName) => + static String m57(providerName) => "Vui lòng nói chuyện với bộ phận hỗ trợ ${providerName} nếu bạn đã bị tính phí"; - static String m61(endDate) => + static String m62(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}.\nBạn có thể chọn gói trả phí sau đó."; - static String m62(toEmail) => + static String m63(toEmail) => "Vui lòng gửi email cho chúng tôi tại ${toEmail}"; - static String m63(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; + static String m64(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; - static String m65(folderName) => "Đang xử lý ${folderName}..."; + static String m66(folderName) => "Đang xử lý ${folderName}..."; - static String m66(storeName) => "Đánh giá chúng tôi trên ${storeName}"; + static String m67(storeName) => "Đánh giá chúng tôi trên ${storeName}"; - static String m68(days, email) => + static String m69(days, email) => "Bạn có thể truy cập tài khoản sau ${days} ngày. Một thông báo sẽ được gửi đến ${email}."; - static String m69(email) => + static String m70(email) => "Bạn có thể khôi phục tài khoản của ${email} bằng cách đặt lại mật khẩu mới."; - static String m70(email) => + static String m71(email) => "${email} đang cố gắng khôi phục tài khoản của bạn."; - static String m71(storageInGB) => + static String m72(storageInGB) => "3. Cả hai bạn đều nhận ${storageInGB} GB* miễn phí"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} sẽ bị xóa khỏi album chia sẻ này\n\nBất kỳ ảnh nào được thêm bởi họ cũng sẽ bị xóa khỏi album"; - static String m73(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; + static String m74(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, one: '${count} kết quả được tìm thấy', other: '${count} kết quả được tìm thấy')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "Độ dài các phần không khớp: ${snapshotLength} != ${searchLength}"; - static String m76(count) => "${count} đã chọn"; + static String m78(count) => "${count} đã chọn"; - static String m77(count, yourCount) => + static String m79(count, yourCount) => "${count} đã chọn (${yourCount} của bạn)"; - static String m79(verificationID) => + static String m81(verificationID) => "Đây là ID xác minh của tôi: ${verificationID} cho ente.io."; - static String m80(verificationID) => + static String m82(verificationID) => "Chào, bạn có thể xác nhận rằng đây là ID xác minh ente.io của bạn: ${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Mã giới thiệu Ente: ${referralCode} \n\nÁp dụng nó trong Cài đặt → Chung → Giới thiệu để nhận ${referralStorageInGB} GB miễn phí sau khi bạn đăng ký gói trả phí\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Chia sẻ với những người cụ thể', one: 'Chia sẻ với 1 người', other: 'Chia sẻ với ${numberOfPeople} người')}"; - static String m83(emailIDs) => "Chia sẻ với ${emailIDs}"; + static String m85(emailIDs) => "Chia sẻ với ${emailIDs}"; - static String m84(fileType) => + static String m86(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi thiết bị của bạn."; - static String m85(fileType) => + static String m87(fileType) => "Tệp ${fileType} này có trong cả Ente và thiết bị của bạn."; - static String m86(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; + static String m88(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} trong tổng số ${totalAmount} ${totalStorageUnit} đã sử dụng"; - static String m91(id) => + static String m93(id) => "ID ${id} của bạn đã được liên kết với một tài khoản Ente khác.\nNếu bạn muốn sử dụng ID ${id} này với tài khoản này, vui lòng liên hệ với bộ phận hỗ trợ của chúng tôi."; - static String m92(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; + static String m94(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; - static String m93(completed, total) => + static String m95(completed, total) => "${completed}/${total} kỷ niệm đã được lưu giữ"; - static String m94(ignoreReason) => + static String m96(ignoreReason) => "Nhấn để tải lên, tải lên hiện tại bị bỏ qua do ${ignoreReason}"; - static String m95(storageAmountInGB) => + static String m97(storageAmountInGB) => "Họ cũng nhận được ${storageAmountInGB} GB"; - static String m96(email) => "Đây là ID xác minh của ${email}"; + static String m98(email) => "Đây là ID xác minh của ${email}"; - static String m99(count) => + static String m101(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m102(email) => + static String m104(email) => "Bạn đã được mời làm người liên hệ thừa kế bởi ${email}."; - static String m103(galleryType) => + static String m105(galleryType) => "Loại thư viện ${galleryType} không được hỗ trợ để đổi tên"; - static String m104(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; + static String m106(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; - static String m105(count) => "Đang lưu giữ ${count} kỷ niệm..."; + static String m107(count) => "Đang lưu giữ ${count} kỷ niệm..."; - static String m106(endDate) => "Có hiệu lực đến ${endDate}"; + static String m108(endDate) => "Có hiệu lực đến ${endDate}"; - static String m107(email) => "Xác minh ${email}"; + static String m109(email) => "Xác minh ${email}"; - static String m109(email) => + static String m112(email) => "Chúng tôi đã gửi một email đến ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} năm trước', other: '${count} năm trước')}"; - static String m112(storageSaved) => + static String m115(storageSaved) => "Bạn đã giải phóng thành công ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -449,6 +449,7 @@ class MessageLookup extends MessageLookupByLibrary { "Các mục đã được sao lưu sẽ hiển thị ở đây"), "backupVideos": MessageLookupByLibrary.simpleMessage("Sao lưu video"), "birthday": MessageLookupByLibrary.simpleMessage("Sinh nhật"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Giảm giá Black Friday"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), @@ -520,6 +521,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• Nhấn"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• Nhấn vào menu thả xuống"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("Đóng"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("Nhóm theo thời gian chụp"), @@ -758,8 +761,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email đã được đăng kí."), - "emailChangedTo": m27, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email chưa được đăng kí."), "emailVerificationToggle": @@ -840,7 +843,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Xuất dữ liệu của bạn"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Tìm thấy ảnh bổ sung"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Khuôn mặt chưa được phân cụm, vui lòng quay lại sau"), "faceRecognition": @@ -889,8 +892,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Loại tệp"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Loại tệp và tên"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("Tệp đã bị xóa"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( "Các tệp đã được lưu vào thư viện"), @@ -906,12 +909,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Đã tìm thấy khuôn mặt"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Lưu trữ miễn phí đã yêu cầu"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Lưu trữ miễn phí có thể sử dụng"), "freeTrial": MessageLookupByLibrary.simpleMessage("Dùng thử miễn phí"), - "freeTrialValidTill": m36, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Giải phóng không gian thiết bị"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( @@ -924,7 +927,7 @@ class MessageLookup extends MessageLookupByLibrary { "general": MessageLookupByLibrary.simpleMessage("Chung"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("Đang tạo khóa mã hóa..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("Đi đến cài đặt"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -935,6 +938,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("Chế độ khách"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Để bật chế độ khách, vui lòng thiết lập mã khóa thiết bị hoặc khóa màn hình trong cài đặt hệ thống của bạn."), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Chúng tôi không theo dõi cài đặt ứng dụng. Sẽ rất hữu ích nếu bạn cho chúng tôi biết bạn đã tìm thấy chúng ở đâu!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -1005,7 +1010,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Có vẻ như đã xảy ra sự cố. Vui lòng thử lại sau một thời gian. Nếu lỗi vẫn tiếp diễn, vui lòng liên hệ với đội ngũ hỗ trợ của chúng tôi."), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Các mục cho biết số ngày còn lại trước khi xóa vĩnh viễn"), @@ -1035,7 +1040,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Thừa kế"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Tài khoản thừa kế"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Thừa kế cho phép các liên hệ tin cậy truy cập tài khoản của bạn khi bạn không hoạt động."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1048,7 +1053,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Giới hạn thiết bị"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Đã bật"), "linkExpired": MessageLookupByLibrary.simpleMessage("Hết hạn"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Hết hạn liên kết"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Liên kết đã hết hạn"), @@ -1057,8 +1062,6 @@ class MessageLookup extends MessageLookupByLibrary { "livePhotos": MessageLookupByLibrary.simpleMessage("Ảnh trực tiếp"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Bạn có thể chia sẻ đăng ký của mình với gia đình"), - "loadMessage2": MessageLookupByLibrary.simpleMessage( - "Chúng tôi đã bảo tồn hơn 30 triệu kỷ niệm cho đến nay"), "loadMessage3": MessageLookupByLibrary.simpleMessage( "Chúng tôi giữ 3 bản sao dữ liệu của bạn, một trong nơi trú ẩn dưới lòng đất"), "loadMessage4": MessageLookupByLibrary.simpleMessage( @@ -1113,6 +1116,8 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Nhấn và giữ vào một mục để xem toàn màn hình"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("Dừng phát video lặp lại"), "loopVideoOn": @@ -1170,7 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary { "moveToAlbum": MessageLookupByLibrary.simpleMessage("Chuyển đến album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Di chuyển đến album ẩn"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("Đã chuyển vào thùng rác"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1186,6 +1191,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("Album mới"), "newLocation": MessageLookupByLibrary.simpleMessage("Vị trí mới"), "newPerson": MessageLookupByLibrary.simpleMessage("Người mới"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newToEnte": MessageLookupByLibrary.simpleMessage("Mới đến Ente"), "newest": MessageLookupByLibrary.simpleMessage("Mới nhất"), "next": MessageLookupByLibrary.simpleMessage("Tiếp theo"), @@ -1223,10 +1229,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Không có kết quả"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Không tìm thấy kết quả"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Không tìm thấy khóa hệ thống"), - "notPersonLabel": m52, + "notPersonLabel": m53, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Chưa có gì được chia sẻ với bạn"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1236,7 +1242,10 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Trên thiết bị"), "onEnte": MessageLookupByLibrary.simpleMessage( "Trên ente"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("Chỉ họ"), "oops": MessageLookupByLibrary.simpleMessage("Ôi"), "oopsCouldNotSaveEdits": @@ -1273,7 +1282,7 @@ class MessageLookup extends MessageLookupByLibrary { "Đã thay đổi mật khẩu thành công"), "passwordLock": MessageLookupByLibrary.simpleMessage("Khóa bằng mật khẩu"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "Độ mạnh của mật khẩu được tính toán dựa trên độ dài của mật khẩu, các ký tự đã sử dụng và liệu mật khẩu có xuất hiện trong 10.000 mật khẩu được sử dụng nhiều nhất hay không"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1284,7 +1293,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Thanh toán thất bại"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Rất tiếc, thanh toán của bạn đã thất bại. Vui lòng liên hệ với bộ phận hỗ trợ và chúng tôi sẽ giúp bạn!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("Các mục đang chờ"), "pendingSync": @@ -1312,7 +1321,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Ghim album"), "pinLock": MessageLookupByLibrary.simpleMessage("Khóa PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Phát album trên TV"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Đăng ký PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1324,14 +1333,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vui lòng liên hệ với bộ phận hỗ trợ nếu vấn đề vẫn tiếp diễn"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Vui lòng cấp quyền"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Vui lòng đăng nhập lại"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Vui lòng chọn liên kết nhanh để xóa"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Vui lòng thử lại"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1360,7 +1369,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Chia sẻ riêng tư"), "proceed": MessageLookupByLibrary.simpleMessage("Tiếp tục"), "processed": MessageLookupByLibrary.simpleMessage("Đã xử lý"), - "processingImport": m65, + "processingImport": m66, "publicLinkCreated": MessageLookupByLibrary.simpleMessage( "Liên kết công khai đã được tạo"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1370,7 +1379,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Tạo vé"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Đánh giá ứng dụng"), "rateUs": MessageLookupByLibrary.simpleMessage("Đánh giá chúng tôi"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("Khôi phục"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Khôi phục tài khoản"), @@ -1379,7 +1390,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Khôi phục tài khoản"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage( "Quá trình khôi phục đã được khởi động"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("Khóa khôi phục"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Khóa khôi phục đã được sao chép vào clipboard"), @@ -1393,12 +1404,12 @@ class MessageLookup extends MessageLookupByLibrary { "Khóa khôi phục đã được xác minh"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Khóa khôi phục của bạn là cách duy nhất để khôi phục ảnh của bạn nếu bạn quên mật khẩu. Bạn có thể tìm thấy khóa khôi phục của mình trong Cài đặt > Tài khoản.\n\nVui lòng nhập khóa khôi phục của bạn ở đây để xác minh rằng bạn đã lưu nó đúng cách."), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Khôi phục thành công!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Một liên hệ tin cậy đang cố gắng truy cập tài khoản của bạn"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Thiết bị hiện tại không đủ mạnh để xác minh mật khẩu của bạn, nhưng chúng tôi có thể tạo lại theo cách hoạt động với tất cả các thiết bị.\n\nVui lòng đăng nhập bằng khóa khôi phục của bạn và tạo lại mật khẩu (bạn có thể sử dụng lại mật khẩu cũ nếu muốn)."), "recreatePasswordTitle": @@ -1413,7 +1424,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Đưa mã này cho bạn bè của bạn"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. Họ đăng ký gói trả phí"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("Giới thiệu"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Giới thiệu hiện đang tạm dừng"), @@ -1442,7 +1453,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Xóa liên kết"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Xóa người tham gia"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Xóa nhãn người"), "removePublicLink": @@ -1461,7 +1472,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Đổi tên tệp"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Gia hạn đăng ký"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("Báo cáo lỗi"), "reportBug": MessageLookupByLibrary.simpleMessage("Báo cáo lỗi"), "resendEmail": MessageLookupByLibrary.simpleMessage("Gửi lại email"), @@ -1537,8 +1548,8 @@ class MessageLookup extends MessageLookupByLibrary { "Mời mọi người, và bạn sẽ thấy tất cả ảnh được chia sẻ bởi họ ở đây"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Người sẽ được hiển thị ở đây sau khi hoàn tất xử lý và đồng bộ"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("Bảo mật"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Xem liên kết album công khai trong ứng dụng"), @@ -1571,8 +1582,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Các mục đã chọn sẽ bị xóa khỏi tất cả các album và chuyển vào thùng rác."), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, "send": MessageLookupByLibrary.simpleMessage("Gửi"), "sendEmail": MessageLookupByLibrary.simpleMessage("Gửi email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Gửi lời mời"), @@ -1603,16 +1614,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Chia sẻ một album ngay bây giờ"), "shareLink": MessageLookupByLibrary.simpleMessage("Chia sẻ liên kết"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Chia sẻ chỉ với những người bạn muốn"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Tải Ente để chúng ta có thể dễ dàng chia sẻ ảnh và video chất lượng gốc\n\nhttps://ente.io"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Chia sẻ với người dùng không phải Ente"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Chia sẻ album đầu tiên của bạn"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1624,7 +1635,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ảnh chia sẻ mới"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Nhận thông báo khi ai đó thêm ảnh vào album chia sẻ mà bạn tham gia"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Chia sẻ với tôi"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Được chia sẻ với bạn"), @@ -1640,11 +1651,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Đăng xuất các thiết bị khác"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Tôi đồng ý với các điều khoản dịch vụchính sách bảo mật"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Nó sẽ bị xóa khỏi tất cả các album."), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("Bỏ qua"), "social": MessageLookupByLibrary.simpleMessage("Xã hội"), "someItemsAreInBothEnteAndYourDevice": @@ -1673,6 +1684,8 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Xin lỗi, chúng tôi không thể tạo khóa an toàn trên thiết bị này.\n\nVui lòng đăng ký từ một thiết bị khác."), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("Sắp xếp"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sắp xếp theo"), "sortNewestFirst": @@ -1691,13 +1704,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Gia đình"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Bạn"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Vượt quá giới hạn lưu trữ"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "strongStrength": MessageLookupByLibrary.simpleMessage("Mạnh"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("Đăng ký"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Bạn cần một đăng ký trả phí hoạt động để kích hoạt chia sẻ."), @@ -1714,7 +1727,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Gợi ý tính năng"), "support": MessageLookupByLibrary.simpleMessage("Hỗ trợ"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("Đồng bộ hóa đã dừng"), "syncing": MessageLookupByLibrary.simpleMessage("Đang đồng bộ hóa..."), @@ -1724,7 +1737,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Chạm để nhập mã"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("Nhấn để mở khóa"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Nhấn để tải lên"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Có vẻ như đã xảy ra sự cố. Vui lòng thử lại sau một thời gian. Nếu lỗi vẫn tiếp diễn, vui lòng liên hệ với đội ngũ hỗ trợ."), "terminate": MessageLookupByLibrary.simpleMessage("Kết thúc"), @@ -1748,7 +1761,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Các mục này sẽ bị xóa khỏi thiết bị của bạn."), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Chúng sẽ bị xóa khỏi tất cả các album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1764,7 +1777,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Email này đã được sử dụng"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Hình ảnh này không có dữ liệu exif"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Đây là ID xác minh của bạn"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1788,11 +1801,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("tổng"), "totalSize": MessageLookupByLibrary.simpleMessage("Tổng kích thước"), "trash": MessageLookupByLibrary.simpleMessage("Thùng rác"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("Cắt"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Liên hệ tin cậy"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("Thử lại"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Bật sao lưu để tự động tải lên các tệp được thêm vào thư mục thiết bị này lên Ente."), @@ -1811,7 +1824,7 @@ class MessageLookup extends MessageLookupByLibrary { "Xác thực hai yếu tố đã được đặt lại thành công"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Cài đặt hai yếu tố"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("Khôi phục"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Khôi phục album"), @@ -1835,10 +1848,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Đang cập nhật lựa chọn thư mục..."), "upgrade": MessageLookupByLibrary.simpleMessage("Nâng cấp"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Đang tải tệp lên album..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Đang lưu giữ 1 kỷ niệm..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1856,14 +1869,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sử dụng ảnh đã chọn"), "usedSpace": MessageLookupByLibrary.simpleMessage("Không gian đã sử dụng"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Xác minh không thành công, vui lòng thử lại"), "verificationId": MessageLookupByLibrary.simpleMessage("ID xác minh"), "verify": MessageLookupByLibrary.simpleMessage("Xác minh"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Xác minh email"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Xác minh"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Xác minh mã khóa"), @@ -1901,7 +1914,7 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Chúng tôi không hỗ trợ chỉnh sửa ảnh và album mà bạn chưa sở hữu"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("Yếu"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Chào mừng trở lại!"), @@ -1910,7 +1923,7 @@ class MessageLookup extends MessageLookupByLibrary { "Liên hệ tin cậy có thể giúp khôi phục dữ liệu của bạn."), "yearShort": MessageLookupByLibrary.simpleMessage("năm"), "yearly": MessageLookupByLibrary.simpleMessage("Hàng năm"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("Có"), "yesCancel": MessageLookupByLibrary.simpleMessage("Có, hủy"), "yesConvertToViewer": @@ -1942,7 +1955,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bạn không thể chia sẻ với chính mình"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Bạn không có mục nào đã lưu trữ."), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tài khoản của bạn đã bị xóa"), "yourMap": MessageLookupByLibrary.simpleMessage("Bản đồ của bạn"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 4125bcafcc..d41df771af 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -93,203 +93,203 @@ class MessageLookup extends MessageLookupByLibrary { static String m26(count, formattedSize) => "${count} 个文件,每个文件 ${formattedSize}"; - static String m27(newEmail) => "电子邮件已更改为 ${newEmail}"; + static String m28(newEmail) => "电子邮件已更改为 ${newEmail}"; - static String m28(email) => "${email} 没有 Ente 账户。"; + static String m29(email) => "${email} 没有 Ente 账户。"; - static String m29(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; + static String m30(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; - static String m30(name) => "拥抱 ${name}"; + static String m31(name) => "拥抱 ${name}"; - static String m31(text) => "为 ${text} 找到额外照片"; + static String m32(text) => "为 ${text} 找到额外照片"; - static String m32(name) => "与 ${name} 的盛宴"; - - static String m33(count, formattedNumber) => - "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; + static String m33(name) => "与 ${name} 的盛宴"; static String m34(count, formattedNumber) => + "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; + + static String m35(count, formattedNumber) => "此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; - static String m35(storageAmountInGB) => + static String m36(storageAmountInGB) => "每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB"; - static String m36(endDate) => "免费试用有效期至 ${endDate}"; + static String m37(endDate) => "免费试用有效期至 ${endDate}"; - static String m37(count) => + static String m38(count) => "只要您拥有有效订阅,您仍然可以在 Ente 上访问 ${Intl.plural(count, one: '它', other: '它们')}"; - static String m38(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; + static String m39(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; - static String m39(count, formattedSize) => + static String m40(count, formattedSize) => "${Intl.plural(count, one: '它可以从设备中删除以释放 ${formattedSize}', other: '它们可以从设备中删除以释放 ${formattedSize}')}"; - static String m40(currentlyProcessing, totalCount) => + static String m41(currentlyProcessing, totalCount) => "正在处理 ${currentlyProcessing} / ${totalCount}"; - static String m41(name) => "与 ${name} 徒步"; + static String m42(name) => "与 ${name} 徒步"; - static String m42(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} 个项目', other: '${count} 个项目')}"; - static String m43(name) => "最后一次与 ${name} 相聚"; + static String m44(name) => "最后一次与 ${name} 相聚"; - static String m44(email) => "${email} 已邀请您成为可信联系人"; + static String m45(email) => "${email} 已邀请您成为可信联系人"; - static String m45(expiryTime) => "链接将在 ${expiryTime} 过期"; + static String m46(expiryTime) => "链接将在 ${expiryTime} 过期"; - static String m46(email) => "将人员链接到 ${email}"; + static String m47(email) => "将人员链接到 ${email}"; - static String m47(personName, email) => "这将会将 ${personName} 链接到 ${email}"; + static String m48(personName, email) => "这将会将 ${personName} 链接到 ${email}"; - static String m48(count, formattedCount) => + static String m49(count, formattedCount) => "${Intl.plural(count, zero: '暂无回忆', other: '${formattedCount} 个回忆')}"; - static String m49(count) => + static String m50(count) => "${Intl.plural(count, one: '移动项目', other: '移动数个项目')}"; - static String m50(albumName) => "成功移动到 ${albumName}"; + static String m51(albumName) => "成功移动到 ${albumName}"; - static String m51(personName) => "没有针对 ${personName} 的建议"; + static String m52(personName) => "没有针对 ${personName} 的建议"; - static String m52(name) => "不是 ${name}?"; + static String m53(name) => "不是 ${name}?"; - static String m53(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; + static String m54(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; - static String m54(name) => "与 ${name} 开派对"; + static String m55(name) => "与 ${name} 开派对"; - static String m55(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; + static String m56(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; - static String m56(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; + static String m57(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; - static String m57(name, age) => "${name} ${age} 岁啦!"; + static String m58(name, age) => "${name} ${age} 岁啦!"; - static String m58(name, age) => "${name} 快满 ${age} 岁啦"; - - static String m59(count) => - "${Intl.plural(count, zero: '没有照片', one: '1 张照片', other: '${count} 张照片')}"; + static String m59(name, age) => "${name} 快满 ${age} 岁啦"; static String m60(count) => + "${Intl.plural(count, zero: '没有照片', one: '1 张照片', other: '${count} 张照片')}"; + + static String m61(count) => "${Intl.plural(count, zero: '0张照片', one: '1张照片', other: '${count} 张照片')}"; - static String m61(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; + static String m62(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; - static String m62(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; + static String m63(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; - static String m63(toEmail) => "请将日志发送至 \n${toEmail}"; + static String m64(toEmail) => "请将日志发送至 \n${toEmail}"; - static String m64(name) => "与 ${name} 的合影"; + static String m65(name) => "与 ${name} 的合影"; - static String m65(folderName) => "正在处理 ${folderName}..."; + static String m66(folderName) => "正在处理 ${folderName}..."; - static String m66(storeName) => "在 ${storeName} 上给我们评分"; + static String m67(storeName) => "在 ${storeName} 上给我们评分"; - static String m67(name) => "已将您重新分配给 ${name}"; + static String m68(name) => "已将您重新分配给 ${name}"; - static String m68(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; + static String m69(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; - static String m69(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; + static String m70(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; - static String m70(email) => "${email} 正在尝试恢复您的账户。"; + static String m71(email) => "${email} 正在尝试恢复您的账户。"; - static String m71(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; + static String m72(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - static String m72(userEmail) => + static String m73(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; - static String m73(endDate) => "在 ${endDate} 前续费"; + static String m74(endDate) => "在 ${endDate} 前续费"; - static String m74(name) => "与 ${name} 一起的自驾游"; + static String m75(name) => "与 ${name} 一起的自驾游"; - static String m113(count) => + static String m76(count) => "${Intl.plural(count, other: '已找到 ${count} 个结果')}"; - static String m75(snapshotLength, searchLength) => + static String m77(snapshotLength, searchLength) => "部分长度不匹配:${snapshotLength} != ${searchLength}"; - static String m76(count) => "已选择 ${count} 个"; + static String m78(count) => "已选择 ${count} 个"; - static String m77(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; + static String m79(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; - static String m78(name) => "与 ${name} 的自拍"; + static String m80(name) => "与 ${name} 的自拍"; - static String m79(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; + static String m81(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; - static String m80(verificationID) => + static String m82(verificationID) => "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; - static String m81(referralCode, referralStorageInGB) => + static String m83(referralCode, referralStorageInGB) => "Ente 推荐代码:${referralCode}\n\n在 \"设置\"→\"通用\"→\"推荐 \"中应用它,即可在注册付费计划后免费获得 ${referralStorageInGB} GB 存储空间\n\nhttps://ente.io"; - static String m82(numberOfPeople) => + static String m84(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; - static String m83(emailIDs) => "与 ${emailIDs} 共享"; + static String m85(emailIDs) => "与 ${emailIDs} 共享"; - static String m84(fileType) => "此 ${fileType} 将从您的设备中删除。"; + static String m86(fileType) => "此 ${fileType} 将从您的设备中删除。"; - static String m85(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; + static String m87(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; - static String m86(fileType) => "${fileType} 将从 Ente 中删除。"; + static String m88(fileType) => "${fileType} 将从 Ente 中删除。"; - static String m87(name) => "与 ${name} 一起运动"; + static String m89(name) => "与 ${name} 一起运动"; - static String m88(name) => "聚光灯下的 ${name}"; + static String m90(name) => "聚光灯下的 ${name}"; - static String m89(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m91(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m90( + static String m92( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "已使用 ${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit}"; - static String m91(id) => + static String m93(id) => "您的 ${id} 已链接到另一个 Ente 账户。\n如果您想在此账户中使用您的 ${id} ,请联系我们的支持人员"; - static String m92(endDate) => "您的订阅将于 ${endDate} 取消"; + static String m94(endDate) => "您的订阅将于 ${endDate} 取消"; - static String m93(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; + static String m95(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; - static String m94(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; + static String m96(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; - static String m95(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; + static String m97(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; - static String m96(email) => "这是 ${email} 的验证ID"; - - static String m97(count) => - "${Intl.plural(count, one: '${count} 年前的本周', other: '${count} 年前的本周')}"; - - static String m98(dateFormat) => "${dateFormat} 年间"; + static String m98(email) => "这是 ${email} 的验证ID"; static String m99(count) => + "${Intl.plural(count, one: '${count} 年前的本周', other: '${count} 年前的本周')}"; + + static String m100(dateFormat) => "${dateFormat} 年间"; + + static String m101(count) => "${Intl.plural(count, zero: '马上', one: '1 天', other: '${count} 天')}"; - static String m100(year) => "${year} 年的旅行"; + static String m102(year) => "${year} 年的旅行"; - static String m101(location) => "前往 ${location} 的旅行"; + static String m103(location) => "前往 ${location} 的旅行"; - static String m102(email) => "您已受邀通过 ${email} 成为遗产联系人。"; + static String m104(email) => "您已受邀通过 ${email} 成为遗产联系人。"; - static String m103(galleryType) => "相册类型 ${galleryType} 不支持重命名"; + static String m105(galleryType) => "相册类型 ${galleryType} 不支持重命名"; - static String m104(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; + static String m106(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; - static String m105(count) => "正在保存 ${count} 个回忆..."; + static String m107(count) => "正在保存 ${count} 个回忆..."; - static String m106(endDate) => "有效期至 ${endDate}"; + static String m108(endDate) => "有效期至 ${endDate}"; - static String m107(email) => "验证 ${email}"; + static String m109(email) => "验证 ${email}"; - static String m108(count) => + static String m111(count) => "${Intl.plural(count, zero: '已添加0个查看者', one: '已添加1个查看者', other: '已添加 ${count} 个查看者')}"; - static String m109(email) => "我们已经发送邮件到 ${email}"; + static String m112(email) => "我们已经发送邮件到 ${email}"; - static String m110(count) => + static String m113(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m111(name) => "您和 ${name}"; + static String m114(name) => "您和 ${name}"; - static String m112(storageSaved) => "您已成功释放了 ${storageSaved}!"; + static String m115(storageSaved) => "您已成功释放了 ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -477,23 +477,9 @@ class MessageLookup extends MessageLookupByLibrary { "backupVideos": MessageLookupByLibrary.simpleMessage("备份视频"), "beach": MessageLookupByLibrary.simpleMessage("沙滩与大海"), "birthday": MessageLookupByLibrary.simpleMessage("生日"), + "birthdays": MessageLookupByLibrary.simpleMessage("Birthdays"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("黑色星期五特惠"), "blog": MessageLookupByLibrary.simpleMessage("博客"), - "cLBulkEdit": MessageLookupByLibrary.simpleMessage("批量编辑日期"), - "cLBulkEditDesc": MessageLookupByLibrary.simpleMessage( - "你现在可以选择多张照片,一键批量修改日期/时间,并支持日期顺移。"), - "cLFamilyPlan": MessageLookupByLibrary.simpleMessage("家庭计划存储限制"), - "cLFamilyPlanDesc": - MessageLookupByLibrary.simpleMessage("你现在可以为家庭成员设置存储空间使用上限。"), - "cLIcon": MessageLookupByLibrary.simpleMessage("新图标"), - "cLIconDesc": MessageLookupByLibrary.simpleMessage( - "终于迎来了一个全新的应用图标,我们认为它最能代表我们的作品。同时,我们还添加了图标切换功能,所以您可以继续使用旧图标。"), - "cLMemories": MessageLookupByLibrary.simpleMessage("回忆"), - "cLMemoriesDesc": MessageLookupByLibrary.simpleMessage( - "重新发现你的珍贵时刻——聚焦你最爱的亲友、旅行与假期、美妙瞬间等精彩回忆。启用机器学习,标记自己并为朋友命名,享受最佳体验。"), - "cLWidgets": MessageLookupByLibrary.simpleMessage("小组件"), - "cLWidgetsDesc": MessageLookupByLibrary.simpleMessage( - "全新首页小组件,与回忆深度集成。无需打开应用,即可在主屏幕上查看你的特别时刻。"), "cachedData": MessageLookupByLibrary.simpleMessage("缓存数据"), "calculating": MessageLookupByLibrary.simpleMessage("正在计算..."), "canNotOpenBody": @@ -549,6 +535,8 @@ class MessageLookup extends MessageLookupByLibrary { "click": MessageLookupByLibrary.simpleMessage("• 点击"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• 点击溢出菜单"), + "clickToInstallOurBestVersionYet": MessageLookupByLibrary.simpleMessage( + "Click to install our best version yet"), "close": MessageLookupByLibrary.simpleMessage("关闭"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按拍摄时间分组"), "clubByFileName": MessageLookupByLibrary.simpleMessage("按文件名排序"), @@ -739,15 +727,15 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("电子邮件地址"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址已被注册。"), - "emailChangedTo": m27, - "emailDoesNotHaveEnteAccount": m28, - "emailNoEnteAccount": m29, + "emailChangedTo": m28, + "emailDoesNotHaveEnteAccount": m29, + "emailNoEnteAccount": m30, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址未被注册。"), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("电子邮件验证"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("通过电子邮件发送您的日志"), - "embracingThem": m30, + "embracingThem": m31, "emergencyContacts": MessageLookupByLibrary.simpleMessage("紧急联系人"), "empty": MessageLookupByLibrary.simpleMessage("清空"), "emptyTrash": MessageLookupByLibrary.simpleMessage("要清空回收站吗?"), @@ -809,7 +797,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("发现额外照片"), - "extraPhotosFoundFor": m31, + "extraPhotosFoundFor": m32, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage("人脸尚未聚类,请稍后再来"), "faceRecognition": MessageLookupByLibrary.simpleMessage("人脸识别"), @@ -838,7 +826,7 @@ class MessageLookup extends MessageLookupByLibrary { "faq": MessageLookupByLibrary.simpleMessage("常见问题"), "faqs": MessageLookupByLibrary.simpleMessage("常见问题"), "favorite": MessageLookupByLibrary.simpleMessage("收藏"), - "feastingWithThem": m32, + "feastingWithThem": m33, "feedback": MessageLookupByLibrary.simpleMessage("反馈"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileFailedToSaveToGallery": @@ -848,8 +836,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("文件已保存到相册"), "fileTypes": MessageLookupByLibrary.simpleMessage("文件类型"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("文件类型和名称"), - "filesBackedUpFromDevice": m33, - "filesBackedUpInAlbum": m34, + "filesBackedUpFromDevice": m34, + "filesBackedUpInAlbum": m35, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("多个文件已保存到相册"), @@ -861,24 +849,24 @@ class MessageLookup extends MessageLookupByLibrary { "forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"), "foundFaces": MessageLookupByLibrary.simpleMessage("已找到的人脸"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("已领取的免费存储"), - "freeStorageOnReferralSuccess": m35, + "freeStorageOnReferralSuccess": m36, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("可用的免费存储"), "freeTrial": MessageLookupByLibrary.simpleMessage("免费试用"), - "freeTrialValidTill": m36, - "freeUpAccessPostDelete": m37, - "freeUpAmount": m38, + "freeTrialValidTill": m37, + "freeUpAccessPostDelete": m38, + "freeUpAmount": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("释放设备空间"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage("通过清除已备份的文件来节省设备空间。"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("释放空间"), - "freeUpSpaceSaving": m39, + "freeUpSpaceSaving": m40, "gallery": MessageLookupByLibrary.simpleMessage("图库"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage("在图库中显示最多1000个回忆"), "general": MessageLookupByLibrary.simpleMessage("通用"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("正在生成加密密钥..."), - "genericProgress": m40, + "genericProgress": m41, "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": @@ -889,6 +877,8 @@ class MessageLookup extends MessageLookupByLibrary { "guestView": MessageLookupByLibrary.simpleMessage("访客视图"), "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage("要启用访客视图,请在系统设置中设置设备密码或屏幕锁。"), + "happyBirthday": + MessageLookupByLibrary.simpleMessage("Happy birthday! 🥳"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"), "hearUsWhereTitle": @@ -904,7 +894,7 @@ class MessageLookup extends MessageLookupByLibrary { "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage("隐藏主页图库中的共享项目"), "hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."), - "hikingWithThem": m41, + "hikingWithThem": m42, "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("法国 OSM 主办"), "howItWorks": MessageLookupByLibrary.simpleMessage("工作原理"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( @@ -952,7 +942,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), - "itemCount": m42, + "itemCount": m43, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"), "itemsWillBeRemovedFromAlbum": @@ -970,7 +960,7 @@ class MessageLookup extends MessageLookupByLibrary { "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("请帮助我们了解这个信息"), "language": MessageLookupByLibrary.simpleMessage("语言"), - "lastTimeWithThem": m43, + "lastTimeWithThem": m44, "lastUpdated": MessageLookupByLibrary.simpleMessage("最后更新"), "lastYearsTrip": MessageLookupByLibrary.simpleMessage("去年的旅行"), "leave": MessageLookupByLibrary.simpleMessage("离开"), @@ -980,7 +970,7 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("向左"), "legacy": MessageLookupByLibrary.simpleMessage("遗产"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("遗产账户"), - "legacyInvite": m44, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage("遗产允许信任的联系人在您不在时访问您的账户。"), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -996,18 +986,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("来实现更快的共享"), "linkEnabled": MessageLookupByLibrary.simpleMessage("已启用"), "linkExpired": MessageLookupByLibrary.simpleMessage("已过期"), - "linkExpiresOn": m45, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("链接过期"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("链接已过期"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("永不"), "linkPerson": MessageLookupByLibrary.simpleMessage("链接人员"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage("来感受更好的共享体验"), - "linkPersonToEmail": m46, - "linkPersonToEmailConfirmation": m47, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("实况照片"), "loadMessage1": MessageLookupByLibrary.simpleMessage("您可以与家庭分享您的订阅"), - "loadMessage2": - MessageLookupByLibrary.simpleMessage("到目前为止,我们已经保存了超过3 000万个回忆"), "loadMessage3": MessageLookupByLibrary.simpleMessage("我们保存你的3个数据副本,其中一个在地下安全屋中"), "loadMessage4": MessageLookupByLibrary.simpleMessage("我们所有的应用程序都是开源的"), @@ -1054,6 +1042,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("长按电子邮件以验证端到端加密。"), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage("长按一个项目来全屏查看"), + "lookBackOnYourMemories": MessageLookupByLibrary.simpleMessage( + "Look back on your memories 🌄"), "loopVideoOff": MessageLookupByLibrary.simpleMessage("循环播放视频关闭"), "loopVideoOn": MessageLookupByLibrary.simpleMessage("循环播放视频开启"), "lostDevice": MessageLookupByLibrary.simpleMessage("设备丢失?"), @@ -1076,7 +1066,7 @@ class MessageLookup extends MessageLookupByLibrary { "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "me": MessageLookupByLibrary.simpleMessage("我"), - "memoryCount": m48, + "memoryCount": m49, "merchandise": MessageLookupByLibrary.simpleMessage("商品"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("与现有的合并"), "mergedPhotos": MessageLookupByLibrary.simpleMessage("已合并照片"), @@ -1103,12 +1093,12 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("最近"), "mostRelevant": MessageLookupByLibrary.simpleMessage("最相关"), "mountains": MessageLookupByLibrary.simpleMessage("翻过山丘"), - "moveItem": m49, + "moveItem": m50, "moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage("将选定的照片调整到某一日期"), "moveToAlbum": MessageLookupByLibrary.simpleMessage("移动到相册"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("移至隐藏相册"), - "movedSuccessfullyTo": m50, + "movedSuccessfullyTo": m51, "movedToTrash": MessageLookupByLibrary.simpleMessage("已移至回收站"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件移动到相册..."), @@ -1122,6 +1112,7 @@ class MessageLookup extends MessageLookupByLibrary { "newAlbum": MessageLookupByLibrary.simpleMessage("新建相册"), "newLocation": MessageLookupByLibrary.simpleMessage("新位置"), "newPerson": MessageLookupByLibrary.simpleMessage("新人物"), + "newPhotosEmoji": MessageLookupByLibrary.simpleMessage(" new 📸"), "newRange": MessageLookupByLibrary.simpleMessage("新起始图片"), "newToEnte": MessageLookupByLibrary.simpleMessage("初来 Ente"), "newest": MessageLookupByLibrary.simpleMessage("最新"), @@ -1152,9 +1143,9 @@ class MessageLookup extends MessageLookupByLibrary { "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密"), "noResults": MessageLookupByLibrary.simpleMessage("无结果"), "noResultsFound": MessageLookupByLibrary.simpleMessage("未找到任何结果"), - "noSuggestionsForPerson": m51, + "noSuggestionsForPerson": m52, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("未找到系统锁"), - "notPersonLabel": m52, + "notPersonLabel": m53, "notThisPerson": MessageLookupByLibrary.simpleMessage("不是此人?"), "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("尚未与您共享任何内容"), @@ -1165,7 +1156,10 @@ class MessageLookup extends MessageLookupByLibrary { "onEnte": MessageLookupByLibrary.simpleMessage( "在 ente 上"), "onTheRoad": MessageLookupByLibrary.simpleMessage("再次踏上旅途"), - "onlyFamilyAdminCanChangeCode": m53, + "onThisDay": MessageLookupByLibrary.simpleMessage("On this day"), + "onThisDayNotificationExplanation": MessageLookupByLibrary.simpleMessage( + "Receive reminders about memories from this day in previous years."), + "onlyFamilyAdminCanChangeCode": m54, "onlyThem": MessageLookupByLibrary.simpleMessage("仅限他们"), "oops": MessageLookupByLibrary.simpleMessage("哎呀"), "oopsCouldNotSaveEdits": @@ -1192,7 +1186,7 @@ class MessageLookup extends MessageLookupByLibrary { "pairWithPin": MessageLookupByLibrary.simpleMessage("用 PIN 配对"), "pairingComplete": MessageLookupByLibrary.simpleMessage("配对完成"), "panorama": MessageLookupByLibrary.simpleMessage("全景"), - "partyWithThem": m54, + "partyWithThem": m55, "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage("仍需进行验证"), "passkey": MessageLookupByLibrary.simpleMessage("通行密钥"), @@ -1201,7 +1195,7 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("密码修改成功"), "passwordLock": MessageLookupByLibrary.simpleMessage("密码锁"), - "passwordStrength": m55, + "passwordStrength": m56, "passwordStrengthInfo": MessageLookupByLibrary.simpleMessage( "密码强度的计算考虑了密码的长度、使用的字符以及密码是否出现在最常用的 10,000 个密码中"), "passwordWarning": MessageLookupByLibrary.simpleMessage( @@ -1210,7 +1204,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"), - "paymentFailedTalkToProvider": m56, + "paymentFailedTalkToProvider": m57, "pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"), "pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1220,18 +1214,18 @@ class MessageLookup extends MessageLookupByLibrary { "permanentlyDelete": MessageLookupByLibrary.simpleMessage("永久删除"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage("要从设备中永久删除吗?"), - "personIsAge": m57, + "personIsAge": m58, "personName": MessageLookupByLibrary.simpleMessage("人物名称"), - "personTurningAge": m58, + "personTurningAge": m59, "pets": MessageLookupByLibrary.simpleMessage("毛茸茸的伙伴"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("照片说明"), "photoGridSize": MessageLookupByLibrary.simpleMessage("照片网格大小"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("照片"), - "photocountPhotos": m59, + "photocountPhotos": m60, "photos": MessageLookupByLibrary.simpleMessage("照片"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"), - "photosCount": m60, + "photosCount": m61, "photosKeepRelativeTimeDifference": MessageLookupByLibrary.simpleMessage("照片保持相对时间差"), "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), @@ -1239,7 +1233,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN 锁定"), "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), "playOriginal": MessageLookupByLibrary.simpleMessage("播放原内容"), - "playStoreFreeTrialValidTill": m61, + "playStoreFreeTrialValidTill": m62, "playStream": MessageLookupByLibrary.simpleMessage("播放流"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), @@ -1250,12 +1244,12 @@ class MessageLookup extends MessageLookupByLibrary { "请用英语联系 support@ente.io ,我们将乐意提供帮助!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("如果问题仍然存在,请联系支持"), - "pleaseEmailUsAt": m62, + "pleaseEmailUsAt": m63, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("请授予权限"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("请重新登录"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("请选择要删除的快速链接"), - "pleaseSendTheLogsTo": m63, + "pleaseSendTheLogsTo": m64, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("请重试"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("请验证您输入的代码"), @@ -1266,7 +1260,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("请稍等片刻后再重试"), "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage("请稍候,这将需要一段时间。"), - "posingWithThem": m64, + "posingWithThem": m65, "preparingLogs": MessageLookupByLibrary.simpleMessage("正在准备日志..."), "preserveMore": MessageLookupByLibrary.simpleMessage("保留更多"), "pressAndHoldToPlayVideo": @@ -1281,7 +1275,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("继续"), "processed": MessageLookupByLibrary.simpleMessage("已处理"), "processing": MessageLookupByLibrary.simpleMessage("正在处理"), - "processingImport": m65, + "processingImport": m66, "processingVideos": MessageLookupByLibrary.simpleMessage("正在处理视频"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公共链接已创建"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("公开链接已启用"), @@ -1291,16 +1285,18 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("提升工单"), "rateTheApp": MessageLookupByLibrary.simpleMessage("为此应用评分"), "rateUs": MessageLookupByLibrary.simpleMessage("给我们评分"), - "rateUsOnStore": m66, + "rateUsOnStore": m67, "reassignMe": MessageLookupByLibrary.simpleMessage("重新分配“我”"), - "reassignedToName": m67, + "reassignedToName": m68, "reassigningLoading": MessageLookupByLibrary.simpleMessage("正在重新分配..."), + "receiveRemindersOnBirthdays": MessageLookupByLibrary.simpleMessage( + "Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person."), "recover": MessageLookupByLibrary.simpleMessage("恢复"), "recoverAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoverButton": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("已启动恢复"), - "recoveryInitiatedDesc": m68, + "recoveryInitiatedDesc": m69, "recoveryKey": MessageLookupByLibrary.simpleMessage("恢复密钥"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("恢复密钥已复制到剪贴板"), @@ -1313,11 +1309,11 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage("恢复密钥已验证"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "如果您忘记了密码,恢复密钥是恢复照片的唯一方法。您可以在“设置”>“账户”中找到恢复密钥。\n\n请在此处输入恢复密钥,以验证您是否已正确保存。"), - "recoveryReady": m69, + "recoveryReady": m70, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("恢复成功!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage("一位可信联系人正在尝试访问您的账户"), - "recoveryWarningBody": m70, + "recoveryWarningBody": m71, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"), @@ -1328,7 +1324,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), - "referralStep3": m71, + "referralStep3": m72, "referrals": MessageLookupByLibrary.simpleMessage("推荐"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("推荐已暂停"), @@ -1351,7 +1347,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeInvite": MessageLookupByLibrary.simpleMessage("移除邀请"), "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), - "removeParticipantBody": m72, + "removeParticipantBody": m73, "removePersonLabel": MessageLookupByLibrary.simpleMessage("移除人物标签"), "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("删除公开链接"), @@ -1366,7 +1362,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("重命名相册"), "renameFile": MessageLookupByLibrary.simpleMessage("重命名文件"), "renewSubscription": MessageLookupByLibrary.simpleMessage("续费订阅"), - "renewsOn": m73, + "renewsOn": m74, "reportABug": MessageLookupByLibrary.simpleMessage("报告错误"), "reportBug": MessageLookupByLibrary.simpleMessage("报告错误"), "resendEmail": MessageLookupByLibrary.simpleMessage("重新发送电子邮件"), @@ -1384,7 +1380,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("请检查并删除您认为重复的项目。"), "reviewSuggestions": MessageLookupByLibrary.simpleMessage("查看建议"), "right": MessageLookupByLibrary.simpleMessage("向右"), - "roadtripWithThem": m74, + "roadtripWithThem": m75, "rotate": MessageLookupByLibrary.simpleMessage("旋转"), "rotateLeft": MessageLookupByLibrary.simpleMessage("向左旋转"), "rotateRight": MessageLookupByLibrary.simpleMessage("向右旋转"), @@ -1429,8 +1425,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage("处理和同步完成后,人物将显示在此处"), - "searchResultCount": m113, - "searchSectionsLengthMismatch": m75, + "searchResultCount": m76, + "searchSectionsLengthMismatch": m77, "security": MessageLookupByLibrary.simpleMessage("安全"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage("在应用程序中查看公开相册链接"), @@ -1466,9 +1462,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"), "selectedItemsWillBeRemovedFromThisPerson": MessageLookupByLibrary.simpleMessage("选定的项目将从此人身上移除,但不会从您的库中删除。"), - "selectedPhotos": m76, - "selectedPhotosWithYours": m77, - "selfiesWithThem": m78, + "selectedPhotos": m78, + "selectedPhotosWithYours": m79, + "selfiesWithThem": m80, "send": MessageLookupByLibrary.simpleMessage("发送"), "sendEmail": MessageLookupByLibrary.simpleMessage("发送电子邮件"), "sendInvite": MessageLookupByLibrary.simpleMessage("发送邀请"), @@ -1491,16 +1487,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开相册并点击右上角的分享按钮进行分享"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("立即分享相册"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), - "shareMyVerificationID": m79, + "shareMyVerificationID": m81, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("仅与您想要的人分享"), - "shareTextConfirmOthersVerificationID": m80, + "shareTextConfirmOthersVerificationID": m82, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage("下载 Ente,让我们轻松共享高质量的原始照片和视频"), - "shareTextReferralCode": m81, + "shareTextReferralCode": m83, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("与非 Ente 用户共享"), - "shareWithPeopleSectionTitle": m82, + "shareWithPeopleSectionTitle": m84, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("分享您的第一个相册"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1511,7 +1507,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新共享的照片"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("当有人将照片添加到您所属的共享相册时收到通知"), - "sharedWith": m83, + "sharedWith": m85, "sharedWithMe": MessageLookupByLibrary.simpleMessage("与我共享"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("已与您共享"), "sharing": MessageLookupByLibrary.simpleMessage("正在分享..."), @@ -1525,11 +1521,11 @@ class MessageLookup extends MessageLookupByLibrary { "signOutOtherDevices": MessageLookupByLibrary.simpleMessage("登出其他设备"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "我同意 服务条款隐私政策"), - "singleFileDeleteFromDevice": m84, + "singleFileDeleteFromDevice": m86, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), - "singleFileInBothLocalAndRemote": m85, - "singleFileInRemoteOnly": m86, + "singleFileInBothLocalAndRemote": m87, + "singleFileInRemoteOnly": m88, "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": @@ -1551,13 +1547,15 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "抱歉,我们无法在此设备上生成安全密钥。\n\n请使用其他设备注册。"), + "sorryWeHadToPauseYourBackups": MessageLookupByLibrary.simpleMessage( + "Sorry, we had to pause your backups"), "sort": MessageLookupByLibrary.simpleMessage("排序"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("排序方式"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("最新在前"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("最旧在前"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ 成功"), - "sportsWithThem": m87, - "spotlightOnThem": m88, + "sportsWithThem": m89, + "spotlightOnThem": m90, "spotlightOnYourself": MessageLookupByLibrary.simpleMessage("聚光灯下的自己"), "startAccountRecoveryTitle": MessageLookupByLibrary.simpleMessage("开始恢复"), @@ -1568,13 +1566,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("存储空间"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("家庭"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), - "storageInGB": m89, + "storageInGB": m91, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("已超出存储限制"), - "storageUsageInfo": m90, + "storageUsageInfo": m92, "streamDetails": MessageLookupByLibrary.simpleMessage("流详情"), "strongStrength": MessageLookupByLibrary.simpleMessage("强"), - "subAlreadyLinkedErrMessage": m91, - "subWillBeCancelledOn": m92, + "subAlreadyLinkedErrMessage": m93, + "subWillBeCancelledOn": m94, "subscribe": MessageLookupByLibrary.simpleMessage("订阅"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage("您需要有效的付费订阅才能启用共享。"), @@ -1588,7 +1586,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("建议新功能"), "sunrise": MessageLookupByLibrary.simpleMessage("在地平线上"), "support": MessageLookupByLibrary.simpleMessage("支持"), - "syncProgress": m93, + "syncProgress": m95, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), @@ -1596,7 +1594,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("点击解锁"), "tapToUpload": MessageLookupByLibrary.simpleMessage("点按上传"), - "tapToUploadIsIgnoredDue": m94, + "tapToUploadIsIgnoredDue": m96, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), @@ -1616,7 +1614,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("主题"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("这些项目将从您的设备中删除。"), - "theyAlsoGetXGb": m95, + "theyAlsoGetXGb": m97, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("他们将从所有相册中删除。"), "thisActionCannotBeUndone": @@ -1631,11 +1629,11 @@ class MessageLookup extends MessageLookupByLibrary { "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("此图像没有Exif 数据"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("这就是我!"), - "thisIsPersonVerificationId": m96, + "thisIsPersonVerificationId": m98, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("这是您的验证 ID"), "thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage("历年本周"), - "thisWeekXYearsAgo": m97, + "thisWeekXYearsAgo": m99, "thisWillLogYouOutOfTheFollowingDevice": MessageLookupByLibrary.simpleMessage("这将使您在以下设备中退出登录:"), "thisWillLogYouOutOfThisDevice": @@ -1644,7 +1642,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这将使所有选定的照片的日期和时间相同。"), "thisWillRemovePublicLinksOfAllSelectedQuickLinks": MessageLookupByLibrary.simpleMessage("这将删除所有选定的快速链接的公共链接。"), - "throughTheYears": m98, + "throughTheYears": m100, "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage("要启用应用锁,请在系统设置中设置设备密码或屏幕锁。"), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("隐藏照片或视频"), @@ -1656,12 +1654,12 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("总计"), "totalSize": MessageLookupByLibrary.simpleMessage("总大小"), "trash": MessageLookupByLibrary.simpleMessage("回收站"), - "trashDaysLeft": m99, + "trashDaysLeft": m101, "trim": MessageLookupByLibrary.simpleMessage("修剪"), - "tripInYear": m100, - "tripToLocation": m101, + "tripInYear": m102, + "tripToLocation": m103, "trustedContacts": MessageLookupByLibrary.simpleMessage("可信联系人"), - "trustedInviteBody": m102, + "trustedInviteBody": m104, "tryAgain": MessageLookupByLibrary.simpleMessage("请再试一次"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "打开备份可自动上传添加到此设备文件夹的文件至 Ente。"), @@ -1676,7 +1674,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("成功重置双重认证"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("双重认证设置"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m103, + "typeOfGallerGallerytypeIsNotSupportedForRename": m105, "unarchive": MessageLookupByLibrary.simpleMessage("取消存档"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("取消存档相册"), "unarchiving": MessageLookupByLibrary.simpleMessage("正在取消存档..."), @@ -1696,10 +1694,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("正在更新文件夹选择..."), "upgrade": MessageLookupByLibrary.simpleMessage("升级"), - "uploadIsIgnoredDueToIgnorereason": m104, + "uploadIsIgnoredDueToIgnorereason": m106, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件上传到相册..."), - "uploadingMultipleMemories": m105, + "uploadingMultipleMemories": m107, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("正在保存 1 个回忆..."), "upto50OffUntil4thDec": @@ -1714,13 +1712,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"), "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"), - "validTill": m106, + "validTill": m108, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("验证失败,请重试"), "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"), "verify": MessageLookupByLibrary.simpleMessage("验证"), "verifyEmail": MessageLookupByLibrary.simpleMessage("验证电子邮件"), - "verifyEmailID": m107, + "verifyEmailID": m109, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("验证"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("验证通行密钥"), "verifyPassword": MessageLookupByLibrary.simpleMessage("验证密码"), @@ -1729,7 +1727,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("正在验证恢复密钥..."), "videoInfo": MessageLookupByLibrary.simpleMessage("视频详情"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("视频"), - "videoStreaming": MessageLookupByLibrary.simpleMessage("影音流"), "videos": MessageLookupByLibrary.simpleMessage("视频"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("查看活动会话"), "viewAddOnButton": MessageLookupByLibrary.simpleMessage("查看附加组件"), @@ -1741,7 +1738,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewLogs": MessageLookupByLibrary.simpleMessage("查看日志"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("查看恢复密钥"), "viewer": MessageLookupByLibrary.simpleMessage("查看者"), - "viewersSuccessfullyAdded": m108, + "viewersSuccessfullyAdded": m111, "visitWebToManage": MessageLookupByLibrary.simpleMessage("请访问 web.ente.io 来管理您的订阅"), "waitingForVerification": @@ -1751,7 +1748,7 @@ class MessageLookup extends MessageLookupByLibrary { "weAreOpenSource": MessageLookupByLibrary.simpleMessage("我们是开源的 !"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage("我们不支持编辑您尚未拥有的照片和相册"), - "weHaveSendEmailTo": m109, + "weHaveSendEmailTo": m112, "weakStrength": MessageLookupByLibrary.simpleMessage("弱"), "welcomeBack": MessageLookupByLibrary.simpleMessage("欢迎回来!"), "whatsNew": MessageLookupByLibrary.simpleMessage("更新日志"), @@ -1759,7 +1756,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("可信联系人可以帮助恢复您的数据。"), "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("每年"), - "yearsAgo": m110, + "yearsAgo": m113, "yes": MessageLookupByLibrary.simpleMessage("是"), "yesCancel": MessageLookupByLibrary.simpleMessage("是的,取消"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("是的,转换为查看者"), @@ -1770,7 +1767,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesRenew": MessageLookupByLibrary.simpleMessage("是的,续费"), "yesResetPerson": MessageLookupByLibrary.simpleMessage("是,重设人物"), "you": MessageLookupByLibrary.simpleMessage("您"), - "youAndThem": m111, + "youAndThem": m114, "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("你在一个家庭计划中!"), "youAreOnTheLatestVersion": @@ -1787,7 +1784,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("莫开玩笑,您不能与自己分享"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("您没有任何存档的项目。"), - "youHaveSuccessfullyFreedUp": m112, + "youHaveSuccessfullyFreedUp": m115, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("您的账户已删除"), "yourMap": MessageLookupByLibrary.simpleMessage("您的地图"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 4f05e7771d..e7b9494cd1 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -60,6 +60,16 @@ class S { ); } + /// `Enter your new email address` + String get enterYourNewEmailAddress { + return Intl.message( + 'Enter your new email address', + name: 'enterYourNewEmailAddress', + desc: '', + args: [], + ); + } + /// `Welcome back!` String get accountWelcomeBack { return Intl.message( @@ -5197,6 +5207,16 @@ class S { ); } + /// `Sorry, we could not backup this file right now, we will retry later.` + String get sorryBackupFailedDesc { + return Intl.message( + 'Sorry, we could not backup this file right now, we will retry later.', + name: 'sorryBackupFailedDesc', + desc: '', + args: [], + ); + } + /// `We could not backup your data.\nWe will retry later.` String get couldNotBackUpTryLater { return Intl.message( @@ -7270,10 +7290,10 @@ class S { ); } - /// `We have preserved over 30 million memories so far` + /// `We have preserved over 200 million memories so far` String get loadMessage2 { return Intl.message( - 'We have preserved over 30 million memories so far', + 'We have preserved over 200 million memories so far', name: 'loadMessage2', desc: '', args: [], @@ -9086,6 +9106,26 @@ class S { ); } + /// `This email is already linked to {name}.` + String editEmailAlreadyLinked(Object name) { + return Intl.message( + 'This email is already linked to $name.', + name: 'editEmailAlreadyLinked', + desc: '', + args: [name], + ); + } + + /// `View {name} to unlink` + String viewPersonToUnlink(Object name) { + return Intl.message( + 'View $name to unlink', + name: 'viewPersonToUnlink', + desc: '', + args: [name], + ); + } + /// `Enter name` String get enterName { return Intl.message( @@ -11071,10 +11111,10 @@ class S { ); } - /// `Video streaming` + /// `Streamable videos` String get videoStreaming { return Intl.message( - 'Video streaming', + 'Streamable videos', name: 'videoStreaming', desc: '', args: [], @@ -11666,111 +11706,271 @@ class S { ); } - /// `New Icon` - String get cLIcon { + /// `Curated memories` + String get curatedMemories { return Intl.message( - 'New Icon', - name: 'cLIcon', - desc: '', - args: [], - ); - } - - /// `Finally, a new app icon, that we think best represents our work. We've also added an icon-switcher so you can continue using the old icon.` - String get cLIconDesc { - return Intl.message( - 'Finally, a new app icon, that we think best represents our work. We\'ve also added an icon-switcher so you can continue using the old icon.', - name: 'cLIconDesc', - desc: '', - args: [], - ); - } - - /// `Memories` - String get cLMemories { - return Intl.message( - 'Memories', - name: 'cLMemories', - desc: '', - args: [], - ); - } - - /// `Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.` - String get cLMemoriesDesc { - return Intl.message( - 'Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.', - name: 'cLMemoriesDesc', + 'Curated memories', + name: 'curatedMemories', desc: '', args: [], ); } /// `Widgets` - String get cLWidgets { + String get widgets { return Intl.message( 'Widgets', - name: 'cLWidgets', + name: 'widgets', desc: '', args: [], ); } - /// `Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.` - String get cLWidgetsDesc { + /// `Memories` + String get memories { return Intl.message( - 'Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.', - name: 'cLWidgetsDesc', + 'Memories', + name: 'memories', desc: '', args: [], ); } - /// `Family Plan Limits` - String get cLFamilyPlan { + /// `Select the people you wish to see on your homescreen.` + String get peopleWidgetDesc { return Intl.message( - 'Family Plan Limits', - name: 'cLFamilyPlan', + 'Select the people you wish to see on your homescreen.', + name: 'peopleWidgetDesc', desc: '', args: [], ); } - /// `You can now set limits on how much storage your family members can use.` - String get cLFamilyPlanDesc { + /// `Select the albums you wish to see on your homescreen.` + String get albumsWidgetDesc { return Intl.message( - 'You can now set limits on how much storage your family members can use.', - name: 'cLFamilyPlanDesc', + 'Select the albums you wish to see on your homescreen.', + name: 'albumsWidgetDesc', desc: '', args: [], ); } - /// `Bulk Edit dates` - String get cLBulkEdit { + /// `Select the kind of memories you wish to see on your homescreen.` + String get memoriesWidgetDesc { return Intl.message( - 'Bulk Edit dates', - name: 'cLBulkEdit', + 'Select the kind of memories you wish to see on your homescreen.', + name: 'memoriesWidgetDesc', desc: '', args: [], ); } - /// `You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.` - String get cLBulkEditDesc { + /// `Smart memories` + String get smartMemories { return Intl.message( - 'You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.', - name: 'cLBulkEditDesc', + 'Smart memories', + name: 'smartMemories', desc: '', args: [], ); } - /// `Curated memories` - String get curatedMemories { + /// `Past years' memories` + String get pastYearsMemories { return Intl.message( - 'Curated memories', - name: 'curatedMemories', + 'Past years\' memories', + name: 'pastYearsMemories', + desc: '', + args: [], + ); + } + + /// `Also delete the photos (and videos) present in these {count} albums from all other albums they are part of?` + String deleteMultipleAlbumDialog(Object count) { + return Intl.message( + 'Also delete the photos (and videos) present in these $count albums from all other albums they are part of?', + name: 'deleteMultipleAlbumDialog', + desc: '', + args: [count], + ); + } + + /// `Add participants` + String get addParticipants { + return Intl.message( + 'Add participants', + name: 'addParticipants', + desc: '', + args: [], + ); + } + + /// `{count} selected` + String selectedAlbums(Object count) { + return Intl.message( + '$count selected', + name: 'selectedAlbums', + desc: '', + args: [count], + ); + } + + /// `Action not supported on Favourites album` + String get actionNotSupportedOnFavouritesAlbum { + return Intl.message( + 'Action not supported on Favourites album', + name: 'actionNotSupportedOnFavouritesAlbum', + desc: '', + args: [], + ); + } + + /// `On this day memories` + String get onThisDayMemories { + return Intl.message( + 'On this day memories', + name: 'onThisDayMemories', + desc: '', + args: [], + ); + } + + /// `On this day` + String get onThisDay { + return Intl.message( + 'On this day', + name: 'onThisDay', + desc: '', + args: [], + ); + } + + /// `Look back on your memories 🌄` + String get lookBackOnYourMemories { + return Intl.message( + 'Look back on your memories 🌄', + name: 'lookBackOnYourMemories', + desc: '', + args: [], + ); + } + + /// ` new 📸` + String get newPhotosEmoji { + return Intl.message( + ' new 📸', + name: 'newPhotosEmoji', + desc: '', + args: [], + ); + } + + /// `Sorry, we had to pause your backups` + String get sorryWeHadToPauseYourBackups { + return Intl.message( + 'Sorry, we had to pause your backups', + name: 'sorryWeHadToPauseYourBackups', + desc: '', + args: [], + ); + } + + /// `Click to install our best version yet` + String get clickToInstallOurBestVersionYet { + return Intl.message( + 'Click to install our best version yet', + name: 'clickToInstallOurBestVersionYet', + desc: '', + args: [], + ); + } + + /// `Receive reminders about memories from this day in previous years.` + String get onThisDayNotificationExplanation { + return Intl.message( + 'Receive reminders about memories from this day in previous years.', + name: 'onThisDayNotificationExplanation', + desc: '', + args: [], + ); + } + + /// `Add a memories widget to your homescreen and come back here to customize.` + String get addMemoriesWidgetPrompt { + return Intl.message( + 'Add a memories widget to your homescreen and come back here to customize.', + name: 'addMemoriesWidgetPrompt', + desc: '', + args: [], + ); + } + + /// `Add an album widget to your homescreen and come back here to customize.` + String get addAlbumWidgetPrompt { + return Intl.message( + 'Add an album widget to your homescreen and come back here to customize.', + name: 'addAlbumWidgetPrompt', + desc: '', + args: [], + ); + } + + /// `Add a people widget to your homescreen and come back here to customize.` + String get addPeopleWidgetPrompt { + return Intl.message( + 'Add a people widget to your homescreen and come back here to customize.', + name: 'addPeopleWidgetPrompt', + desc: '', + args: [], + ); + } + + /// `Birthday notifications` + String get birthdayNotifications { + return Intl.message( + 'Birthday notifications', + name: 'birthdayNotifications', + desc: '', + args: [], + ); + } + + /// `Receive reminders when it's someone's birthday. Tapping on the notification will take you to photos of the birthday person.` + String get receiveRemindersOnBirthdays { + return Intl.message( + 'Receive reminders when it\'s someone\'s birthday. Tapping on the notification will take you to photos of the birthday person.', + name: 'receiveRemindersOnBirthdays', + desc: '', + args: [], + ); + } + + /// `Happy birthday! 🥳` + String get happyBirthday { + return Intl.message( + 'Happy birthday! 🥳', + name: 'happyBirthday', + desc: '', + args: [], + ); + } + + /// `Happy birthday to {name}! 🎉` + String happyBirthdayToPerson(Object name) { + return Intl.message( + 'Happy birthday to $name! 🎉', + name: 'happyBirthdayToPerson', + desc: '', + args: [name], + ); + } + + /// `Birthdays` + String get birthdays { + return Intl.message( + 'Birthdays', + name: 'birthdays', desc: '', args: [], ); @@ -11793,6 +11993,7 @@ class AppLocalizationDelegate extends LocalizationsDelegate { Locale.fromSubtags(languageCode: 'el'), Locale.fromSubtags(languageCode: 'es'), Locale.fromSubtags(languageCode: 'et'), + Locale.fromSubtags(languageCode: 'eu'), Locale.fromSubtags(languageCode: 'fa'), Locale.fromSubtags(languageCode: 'fr'), Locale.fromSubtags(languageCode: 'gu'), @@ -11817,6 +12018,7 @@ class AppLocalizationDelegate extends LocalizationsDelegate { Locale.fromSubtags(languageCode: 'ro'), Locale.fromSubtags(languageCode: 'ru'), Locale.fromSubtags(languageCode: 'sl'), + Locale.fromSubtags(languageCode: 'sr'), Locale.fromSubtags(languageCode: 'sv'), Locale.fromSubtags(languageCode: 'ta'), Locale.fromSubtags(languageCode: 'te'), diff --git a/mobile/lib/l10n/intl_ar.arb b/mobile/lib/l10n/intl_ar.arb index 7816bb9090..c37adfd0c3 100644 --- a/mobile/lib/l10n/intl_ar.arb +++ b/mobile/lib/l10n/intl_ar.arb @@ -1,8 +1,9 @@ { "@@locale ": "en", "enterYourEmailAddress": "أدخل عنوان بريدك الإلكتروني", - "accountWelcomeBack": "مرحبًا مجددًا!", - "emailAlreadyRegistered": "البريد الإلكتروني مُسَجَّل من قبل.", + "enterYourNewEmailAddress": "أدخل عنوان بريدك الإلكتروني الجديد", + "accountWelcomeBack": "أهلاً بعودتك!", + "emailAlreadyRegistered": "البريد الإلكتروني مُسجل من قبل.", "emailNotRegistered": "البريد الإلكتروني غير مسجل.", "email": "البريد الإلكتروني", "cancel": "إلغاء", @@ -17,7 +18,7 @@ "confirmDeletePrompt": "نعم، أرغب في حذف هذا الحساب وبياناته نهائيًا من جميع التطبيقات.", "confirmAccountDeletion": "تأكيد حذف الحساب", "deleteAccountPermanentlyButton": "حذف الحساب نهائيًا", - "yourAccountHasBeenDeleted": "تم حذف حسابك بنجاح.", + "yourAccountHasBeenDeleted": "تم حذف حسابك بنجاح", "selectReason": "اختر سببًا", "deleteReason1": "تفتقر إلى مِيزة أساسية أحتاج إليها", "deleteReason2": "التطبيق أو مِيزة معينة لا تعمل كما هو متوقع", @@ -35,7 +36,7 @@ "activeSessions": "الجلسات النشطة", "oops": "عفوًا", "somethingWentWrongPleaseTryAgain": "حدث خطأ ما، يرجى المحاولة مرة أخرى", - "thisWillLogYouOutOfThisDevice": "سيؤدي هذا إلى تسجيل خروجك من هذا الجهاز.", + "thisWillLogYouOutOfThisDevice": "سيؤدي هذا إلى تسجيل خروجك من هذا الجهاز!", "thisWillLogYouOutOfTheFollowingDevice": "سيؤدي هذا إلى تسجيل خروجك من الجهاز التالي:", "terminateSession": "إنهاء الجَلسةِ؟", "terminate": "إنهاء", @@ -45,7 +46,7 @@ "decrypting": "جارٍ فك التشفير...", "incorrectRecoveryKeyTitle": "مفتاح الاسترداد غير صحيح", "incorrectRecoveryKeyBody": "مفتاح الاسترداد الذي أدخلته غير صحيح", - "forgotPassword": "نسيت كلمة المرور؟", + "forgotPassword": "نسيت كلمة المرور", "enterYourRecoveryKey": "أدخل مفتاح الاسترداد", "noRecoveryKey": "لا تملك مفتاح استرداد؟", "sorry": "عفوًا", @@ -70,7 +71,7 @@ "changePasswordTitle": "تغيير كلمة المرور", "resetPasswordTitle": "إعادة تعيين كلمة المرور", "encryptionKeys": "مفاتيح التشفير", - "passwordWarning": "نحن لا نخزن كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا المساعدة في فك تشفير بياناتك.", + "passwordWarning": "نحن لا نقوم بتخزين كلمة المرور هذه، لذا إذا نسيتها، لا يمكننا فك تشفير بياناتك", "enterPasswordToEncrypt": "أدخل كلمة مرور يمكننا استخدامها لتشفير بياناتك", "enterNewPasswordToEncrypt": "أدخل كلمة مرور جديدة يمكننا استخدامها لتشفير بياناتك", "weakStrength": "ضعيفة", @@ -88,7 +89,7 @@ }, "message": "Password Strength: {passwordStrengthText}" }, - "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح.", + "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", "generatingEncryptionKeys": "جارٍ إنشاء مفاتيح التشفير...", "pleaseWait": "يرجى الانتظار...", "continueLabel": "متابعة", @@ -120,7 +121,7 @@ "recoveryKeyCopiedToClipboard": "تم نسخ مفتاح الاسترداد إلى الحافظة", "recoverAccount": "استعادة الحساب", "recover": "استعادة", - "dropSupportEmail": "يرجى إرسال بريد إلكتروني إلى {supportEmail} من عنوان بريدك الإلكتروني المسجل.", + "dropSupportEmail": "يرجى إرسال بريد إلكتروني إلى {supportEmail} من عنوان بريدك الإلكتروني المسجل", "@dropSupportEmail": { "placeholders": { "supportEmail": { @@ -140,12 +141,12 @@ "enterThe6digitCodeFromnyourAuthenticatorApp": "أدخل الرمز المكون من 6 أرقام من\n تطبيق المصادقة الخاص بك", "confirm": "تأكيد", "setupComplete": "اكتمل الإعداد", - "saveYourRecoveryKeyIfYouHaventAlready": "احفظ مفتاح الاسترداد إذا لم تكن قد فعلت ذلك بالفعل.", - "thisCanBeUsedToRecoverYourAccountIfYou": "يمكن استخدام هذا المفتاح لاستعادة حسابك إذا فقدت جهاز المصادقة الثنائية.", + "saveYourRecoveryKeyIfYouHaventAlready": "احفظ مفتاح الاسترداد إذا لم تكن قد فعلت ذلك", + "thisCanBeUsedToRecoverYourAccountIfYou": "يمكن استخدام هذا المفتاح لاستعادة حسابك إذا فقدت العامل الثاني للمصادقة", "twofactorAuthenticationPageTitle": "المصادقة الثنائية", "lostDevice": "جهاز مفقود؟", "verifyingRecoveryKey": "جارٍ التحقق من مفتاح الاسترداد...", - "recoveryKeyVerified": "تم التحقق من مفتاح الاسترداد.", + "recoveryKeyVerified": "تم التحقق من مفتاح الاسترداد", "recoveryKeySuccessBody": "مفتاح الاسترداد الخاص بك صالح. شكرًا على التحقق.\n\nيرجى تذكر الاحتفاظ بنسخة احتياطية آمنة من مفتاح الاسترداد.", "invalidRecoveryKey": "مفتاح الاسترداد الذي أدخلته غير صالح. يرجى التأكد من أنه يحتوي على 24 كلمة، والتحقق من كتابة كل كلمة بشكل صحيح.\n\nإذا كنت تستخدم مفتاح استرداد قديمًا، تأكد من أنه مكون من 64 حرفًا، وتحقق من صحة كل حرف.", "invalidKey": "المفتاح غير صالح", @@ -190,7 +191,7 @@ "canNotOpenTitle": "لا يمكن فتح هذا الألبوم", "canNotOpenBody": "عذرًا، لا يمكن فتح هذا الألبوم في التطبيق.", "disableDownloadWarningTitle": "يرجى الملاحظة", - "disableDownloadWarningBody": "لا يزال بإمكان المشاهدين التقاط لقطات شاشة أو حفظ نسخة من صورك باستخدام أدوات خارجية.", + "disableDownloadWarningBody": "لا يزال بإمكان المشاهدين التقاط لقطات شاشة أو حفظ نسخة من صورك باستخدام أدوات خارجية", "allowDownloads": "السماح بالتنزيلات", "linkDeviceLimit": "حد الأجهزة", "noDeviceLimit": "لا شيء", @@ -220,7 +221,7 @@ "after1Month": "بعد شهر", "after1Year": "بعد سنة", "manageParticipants": "إدارة المشاركين", - "albumParticipantsCount": "{count, plural, =0 {لا يوجد مشاركون} =1 {مشارك واحد} two {مشاركان} few {{count} مشاركين} many {{count} مشاركًا} other {{count} مشارك}}", + "albumParticipantsCount": "{count, plural, =0 {لا يوجد مُشاركون}=1 {مُشارك واحد} other {{count} مُشاركين}}", "@albumParticipantsCount": { "placeholders": { "count": { @@ -230,7 +231,7 @@ }, "description": "Number of participants in an album, including the album owner." }, - "collabLinkSectionDescription": "أنشئ رابطًا يسمح للأشخاص بإضافة الصور ومشاهدتها في ألبومك المشترك دون الحاجة إلى تطبيق Ente أو حساب. خيار مثالي لجمع صور الفعاليات بسهولة.", + "collabLinkSectionDescription": "أنشئ رابطًا يسمح للأشخاص بإضافة الصور ومشاهدتها في ألبومك المشترك دون الحاجة إلى تطبيق أو حساب Ente. خيار مثالي لجمع صور الفعاليات بسهولة.", "collectPhotos": "جمع الصور", "collaborativeLink": "رابط تعاوني", "shareWithNonenteUsers": "المشاركة مع غير مستخدمي Ente", @@ -241,7 +242,7 @@ "publicLinkEnabled": "تمكين الرابط العام", "shareALink": "مشاركة رابط", "sharedAlbumSectionDescription": "أنشئ ألبومات مشتركة وتعاونية مع مستخدمي Ente الآخرين، بما في ذلك المستخدمين ذوي الاشتراكات المجانية.", - "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {مشاركة مع أشخاص محددين} =1 {تمت المشاركة مع شخص واحد} two {تمت المشاركة مع شخصين} few {تمت المشاركة مع {numberOfPeople} أشخاص} many {تمت المشاركة مع {numberOfPeople} شخصًا} other {تمت المشاركة مع {numberOfPeople} شخصًا}}", + "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {مشاركة مع أشخاص مُحددين}=1 {مُشارَك مع شخص واحد} other {مُشارَك مع {numberOfPeople} أشخاص}}", "@shareWithPeopleSectionTitle": { "placeholders": { "numberOfPeople": { @@ -266,13 +267,13 @@ "verifyEmailID": "التحقق من {email}", "emailNoEnteAccount": "{email} لا يملك حسابًا على Ente.\n\nأرسل له دعوة لمشاركة الصور.", "shareMyVerificationID": "إليك معرّف التحقق الخاص بي لـ ente.io: {verificationID}", - "shareTextConfirmOthersVerificationID": "مرحبًا، هل يمكنك تأكيد أن هذا هو معرّف التحقق الخاص بك على ente.io: {verificationID}؟", + "shareTextConfirmOthersVerificationID": "مرحبًا، هل يمكنك تأكيد أن هذا هو معرّف التحقق الخاص بك على ente.io: {verificationID}", "somethingWentWrong": "حدث خطأ ما", "sendInvite": "إرسال دعوة", "shareTextRecommendUsingEnte": "قم بتنزيل تطبيق Ente حتى نتمكن من مشاركة الصور ومقاطع الفيديو بالجودة الأصلية بسهولة.\n\nhttps://ente.io", "done": "تم", "applyCodeTitle": "تطبيق الرمز", - "enterCodeDescription": "أدخل الرمز المقدم من صديقك للمطالبة بمساحة تخزين مجانية لكما.", + "enterCodeDescription": "أدخل الرمز المقدم من صديقك للمطالبة بمساحة تخزين مجانية لكما", "apply": "تطبيق", "failedToApplyCode": "فشل تطبيق الرمز", "enterReferralCode": "أدخل رمز الإحالة", @@ -354,8 +355,8 @@ "importing": "جارٍ الاستيراد...", "failedToLoadAlbums": "فشل تحميل الألبومات", "hidden": "المخفية", - "authToViewYourHiddenFiles": "يرجى المصادقة للوصول إلى ملفاتك المخفية.", - "authToViewTrashedFiles": "يرجى المصادقة لعرض ملفاتك المحذوفة.", + "authToViewYourHiddenFiles": "يرجى المصادقة للوصول إلى ملفاتك المخفية", + "authToViewTrashedFiles": "يرجى المصادقة لعرض ملفاتك المحذوفة", "trash": "سلة المهملات", "uncategorized": "غير مصنف", "videoSmallCase": "فيديو", @@ -364,7 +365,7 @@ "singleFileInBothLocalAndRemote": "{fileType} موجود في Ente وعلى جهازك.", "singleFileInRemoteOnly": "سيتم حذف {fileType} من Ente.", "singleFileDeleteFromDevice": "سيتم حذف {fileType} من جهازك.", - "deleteFromEnte": "الحذف من Ente", + "deleteFromEnte": "حذف من Ente", "yesDelete": "نعم، حذف", "movedToTrash": "تم النقل إلى سلة المهملات", "deleteFromDevice": "الحذف من الجهاز", @@ -494,14 +495,14 @@ "youAreOnTheLatestVersion": "أنت تستخدم أحدث إصدار.", "account": "الحساب", "manageSubscription": "إدارة الاشتراك", - "authToChangeYourEmail": "يرجى المصادقة لتغيير بريدك الإلكتروني.", + "authToChangeYourEmail": "يرجى المصادقة لتغيير بريدك الإلكتروني", "changePassword": "تغيير كلمة المرور", - "authToChangeYourPassword": "يرجى المصادقة لتغيير كلمة المرور الخاصة بك.", + "authToChangeYourPassword": "يرجى المصادقة لتغيير كلمة المرور الخاصة بك", "emailVerificationToggle": "تأكيد عنوان البريد الإلكتروني", - "authToChangeEmailVerificationSetting": "يرجى المصادقة لتغيير إعداد التحقق من البريد الإلكتروني.", + "authToChangeEmailVerificationSetting": "يرجى المصادقة لتغيير إعداد التحقق من البريد الإلكتروني", "exportYourData": "تصدير بياناتك", "logout": "تسجيل الخروج", - "authToInitiateAccountDeletion": "يرجى المصادقة لبدء عملية حذف الحساب.", + "authToInitiateAccountDeletion": "يرجى المصادقة لبدء عملية حذف الحساب", "areYouSureYouWantToLogout": "هل أنت متأكد من رغبتك في تسجيل الخروج؟", "yesLogout": "نعم، تسجيل الخروج", "aNewVersionOfEnteIsAvailable": "يتوفر إصدار جديد من Ente.", @@ -511,24 +512,24 @@ "updateAvailable": "يتوفر تحديث", "ignoreUpdate": "تجاهل", "downloading": "جارٍ التنزيل...", - "cannotDeleteSharedFiles": "لا يمكن حذف الملفات المشتركة.", - "theDownloadCouldNotBeCompleted": "تعذر إكمال التنزيل.", + "cannotDeleteSharedFiles": "لا يمكن حذف الملفات المشتركة", + "theDownloadCouldNotBeCompleted": "تعذر إكمال التنزيل", "retry": "إعادة المحاولة", "backedUpFolders": "المجلدات المنسوخة احتياطيًا", "backup": "النسخ الاحتياطي", "freeUpDeviceSpace": "تحرير مساحة على الجهاز", "freeUpDeviceSpaceDesc": "وفر مساحة على جهازك عن طريق مسح الملفات التي تم نسخها احتياطيًا.", "allClear": "✨ كل شيء واضح", - "noDeviceThatCanBeDeleted": "لا توجد ملفات على هذا الجهاز يمكن حذفها.", + "noDeviceThatCanBeDeleted": "لا توجد ملفات على هذا الجهاز يمكن حذفها", "removeDuplicates": "إزالة النسخ المكررة", "removeDuplicatesDesc": "مراجعة وإزالة الملفات المتطابقة تمامًا.", "viewLargeFiles": "الملفات الكبيرة", "viewLargeFilesDesc": "عرض الملفات التي تستهلك أكبر قدر من مساحة التخزين.", "noDuplicates": "✨ لا توجد ملفات مكررة", - "youveNoDuplicateFilesThatCanBeCleared": "لا توجد لديك أي ملفات مكررة يمكن مسحها.", + "youveNoDuplicateFilesThatCanBeCleared": "لا توجد لديك أي ملفات مكررة يمكن مسحها", "success": "تم بنجاح", "rateUs": "تقييم التطبيق", - "remindToEmptyDeviceTrash": "تذكر أيضًا إفراغ \"المحذوفة مؤخرًا\" من \"الإعدادات\" -> \"التخزين\" لاستعادة المساحة المحررة.", + "remindToEmptyDeviceTrash": "تذكر أيضًا إفراغ \"المحذوفة مؤخرًا\" من \"الإعدادات\" -> \"التخزين\" لاستعادة المساحة المحررة", "youHaveSuccessfullyFreedUp": "لقد حررت {storageSaved} بنجاح!", "@youHaveSuccessfullyFreedUp": { "description": "The text to display when the user has successfully freed up storage", @@ -598,7 +599,7 @@ "selectYourPlan": "اختر خطتك", "enteSubscriptionPitch": "يحفظ Ente ذكرياتك، بحيث تظل دائمًا متاحة لك حتى لو فقدت جهازك.", "enteSubscriptionShareWithFamily": "يمكنك أيضًا إضافة أفراد عائلتك إلى خطتك.", - "currentUsageIs": "استخدامك الحالي هو", + "currentUsageIs": "استخدامك الحالي هو ", "@currentUsageIs": { "description": "This text is followed by storage usage", "examples": { @@ -682,7 +683,7 @@ "areYouSureYouWantToExit": "هل أنت متأكد من رغبتك في الخروج؟", "thankYou": "شكرًا لك", "failedToVerifyPaymentStatus": "فشل التحقق من حالة الدفع.", - "pleaseWaitForSometimeBeforeRetrying": "يرجى الانتظار لبعض الوقت قبل إعادة المحاولة.", + "pleaseWaitForSometimeBeforeRetrying": "يرجى الانتظار لبعض الوقت قبل إعادة المحاولة", "paymentFailedMessage": "للأسف، فشلت عملية الدفع الخاصة بك. يرجى الاتصال بالدعم وسوف نساعدك!", "youAreOnAFamilyPlan": "أنت مشترك في خطة عائلية!", "contactFamilyAdmin": "يرجى الاتصال بـ {familyAdminEmail} لإدارة اشتراكك.", @@ -691,9 +692,9 @@ "leave": "مغادرة", "rateTheApp": "تقييم التطبيق", "startBackup": "بدء النسخ الاحتياطي", - "noPhotosAreBeingBackedUpRightNow": "لا يتم نسخ أي صور احتياطيًا في الوقت الحالي.", + "noPhotosAreBeingBackedUpRightNow": "لا يتم نسخ أي صور احتياطيًا في الوقت الحالي", "preserveMore": "حفظ المزيد", - "grantFullAccessPrompt": "يرجى السماح بالوصول إلى جميع الصور في تطبيق الإعدادات.", + "grantFullAccessPrompt": "الرجاء السماح بالوصول إلى جميع الصور في تطبيق الإعدادات", "allowPermTitle": "السماح بالوصول إلى الصور", "allowPermBody": "يرجى السماح بالوصول إلى صورك من الإعدادات حتى يتمكن Ente من عرض نسختك الاحتياطية ومكتبتك.", "openSettings": "فتح الإعدادات", @@ -710,10 +711,10 @@ "androidIosWebDesktop": "أندرويد، iOS، الويب، سطح المكتب", "mobileWebDesktop": "الهاتف المحمول، الويب، سطح المكتب", "newToEnte": "جديد في Ente", - "pleaseLoginAgain": "يرجى تسجيل الدخول مرة أخرى.", + "pleaseLoginAgain": "يرجى تسجيل الدخول مرة أخرى", "autoLogoutMessage": "بسبب خلل تقني، تم تسجيل خروجك. نعتذر عن الإزعاج.", - "yourSubscriptionHasExpired": "انتهت صلاحية اشتراكك.", - "storageLimitExceeded": "تم تجاوز حد التخزين.", + "yourSubscriptionHasExpired": "انتهت صلاحية اشتراكك", + "storageLimitExceeded": "تم تجاوز حد التخزين", "upgrade": "ترقية", "raiseTicket": "فتح تذكرة دعم", "@raiseTicket": { @@ -721,9 +722,10 @@ "type": "text" }, "backupFailed": "فشل النسخ الاحتياطي", + "sorryBackupFailedDesc": "عذرًا، لم نتمكن من عمل نسخة احتياطية لهذا الملف الآن، سنعيد المحاولة لاحقًا.", "couldNotBackUpTryLater": "لم نتمكن من نسخ بياناتك احتياطيًا.\nسنحاول مرة أخرى لاحقًا.", - "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "يمكن لـ Ente تشفير وحفظ الملفات فقط إذا منحت الإذن بالوصول إليها.", - "pleaseGrantPermissions": "يرجى منح الأذونات.", + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "يمكن لـ Ente تشفير وحفظ الملفات فقط إذا منحت الإذن بالوصول إليها", + "pleaseGrantPermissions": "يرجى منح الأذونات", "grantPermission": "منح الإذن", "privateSharing": "مشاركة خاصة", "shareOnlyWithThePeopleYouWant": "شارك فقط مع الأشخاص الذين تريدهم.", @@ -1029,7 +1031,7 @@ "didYouKnow": "هل تعلم؟", "loadingMessage": "جارٍ تحميل صورك...", "loadMessage1": "يمكنك مشاركة اشتراكك مع عائلتك.", - "loadMessage2": "لقد حفظنا أكثر من 30 مليون ذكرى حتى الآن.", + "loadMessage2": "لقد حفظنا أكثر من 200 مليون ذكرى حتى الآن", "loadMessage3": "نحتفظ بـ 3 نسخ من بياناتك، إحداها في ملجأ للطوارئ تحت الأرض.", "loadMessage4": "جميع تطبيقاتنا مفتوحة المصدر.", "loadMessage5": "تم تدقيق شفرتنا المصدرية والتشفير الخاص بنا خارجيًا.", @@ -1280,6 +1282,8 @@ "createCollaborativeLink": "إنشاء رابط تعاوني", "search": "بحث", "enterPersonName": "أدخل اسم الشخص", + "editEmailAlreadyLinked": "هذا البريد الإلكتروني مرتبط مسبقاً بـ {name}.", + "viewPersonToUnlink": "عرض {name} لإلغاء الربط", "enterName": "أدخل الاسم", "savePerson": "حفظ الشخص", "editPerson": "تعديل الشخص", @@ -1659,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "بث الفيديو", + "videoStreaming": "مقاطع فيديو قابلة للبث", "processingVideos": "معالجة مقاطع الفيديو", "streamDetails": "تفاصيل البث", "processing": "المعالجة", @@ -1726,15 +1730,15 @@ "onTheRoad": "على الطريق مرة أخرى", "food": "متعة الطهي", "pets": "رفاق فروي", - "cLIcon": "أيقونة جديدة", - "cLIconDesc": "أخيرًا، أيقونة تطبيق جديدة، نعتقد أنها تمثل عملنا على أفضل وجه. أضفنا أيضًا مبدل أيقونات حتى تتمكن من الاستمرار في استخدام الأيقونة القديمة.", - "cLMemories": "الذكريات", - "cLMemoriesDesc": "أعد اكتشاف لحظاتك الخاصة - تسليط الضوء على الأشخاص المفضلين لديك، رحلاتك وعطلاتك، أفضل لقطاتك، وأكثر من ذلك بكثير. قم بتشغيل تعلم الآلة، ضع علامة على نفسك وقم بتسمية أصدقائك للحصول على أفضل تجربة.", - "cLWidgets": "الأدوات المصغرة (Widgets)", - "cLWidgetsDesc": "الأدوات المصغرة للشاشة الرئيسية المدمجة مع الذكريات متاحة الآن. ستعرض لحظاتك الخاصة دون فتح التطبيق.", - "cLFamilyPlan": "حدود الخطة العائلية", - "cLFamilyPlanDesc": "يمكنك الآن تعيين حدود لمقدار التخزين الذي يمكن لأفراد عائلتك استخدامه.", - "cLBulkEdit": "تعديل التواريخ بشكل جماعي", - "cLBulkEditDesc": "يمكنك الآن تحديد صور متعددة، وتعديل التاريخ/الوقت لجميعها بإجراء سريع واحد. تغيير التواريخ مدعوم أيضًا.", - "curatedMemories": "ذكريات منسقة" + "curatedMemories": "ذكريات منسقة", + "memories": "ذكريات", + "deleteMultipleAlbumDialog": "هل تريد أيضًا حذف الصور (والمقاطع) الموجودة في هذه الألبومات {count} من كافة الألبومات الأخرى التي تشترك فيها؟", + "addParticipants": "إضافة مشاركين", + "selectedAlbums": "{count} تم تحديد", + "actionNotSupportedOnFavouritesAlbum": "الإجراء غير مدعوم في ألبوم المفضلة", + "onThisDay": "في هذا اليوم", + "newPhotosEmoji": " جديد 📸", + "happyBirthday": "عيد ميلاد سعيد! 🥳", + "happyBirthdayToPerson": "عيد ميلاد سعيد إلى {name}! 🎉", + "birthdays": "أعياد الميلاد" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 0e1c6c50c9..fa046acab2 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Zadejte svou e-mailovou adresu", "accountWelcomeBack": "Vítejte zpět!", + "emailAlreadyRegistered": "E-mail je již zaregistrován.", + "emailNotRegistered": "E-mail není registrován.", "email": "E-mail", "cancel": "Zrušit", "verify": "Ověřit", @@ -10,66 +12,161 @@ "deleteAccount": "Smazat účet", "askDeleteReason": "Jaký je váš hlavní důvod, proč mažete svůj účet?", "feedback": "Zpětná vazba", + "kindlyHelpUsWithThisInformation": "Pomozte nám s těmito informacemi", + "confirmAccountDeletion": "Potvrdit odstranění účtu", + "deleteAccountPermanentlyButton": "Trvale smazat účet", + "yourAccountHasBeenDeleted": "Váš účet byl smazán", + "selectReason": "Vyberte důvod", + "deleteReason1": "Chybí klíčová funkce, kterou potřebuji", + "deleteReason2": "Aplikace nebo určitá funkce se nechová tak, jak si myslím, že by měla", + "deleteReason3": "Našel jsem jinou službu, která se mi líbí více", + "deleteReason4": "Můj důvod není uveden", + "sendEmail": "Odeslat e-mail", + "deleteRequestSLAText": "Váš požadavek bude zpracován do 72 hodin.", "ok": "Ok", "createAccount": "Vytvořit účet", "createNewAccount": "Vytvořit nový účet", "password": "Heslo", + "activeSessions": "Aktivní relace", + "oops": "Jejda", + "terminateSession": "Ukončit relaci?", "terminate": "Ukončit", "thisDevice": "Toto zařízení", + "recoverButton": "Obnovit", + "recoverySuccessful": "Úspěšně obnoveno!", "decrypting": "Dešifrování...", + "incorrectRecoveryKeyTitle": "Nesprávný obnovovací klíč", "incorrectRecoveryKeyBody": "", + "enterYourRecoveryKey": "Zadejte svůj obnovovací klíč", + "noRecoveryKey": "Nemáte obnovovací klíč?", + "sorry": "Omlouváme se", + "verifyEmail": "Ověřit e-mail", "checkInboxAndSpamFolder": "Zkontrolujte prosím svou doručenou poštu (a spam) pro dokončení ověření", + "resendEmail": "Znovu odeslat e-mail", + "resetPasswordTitle": "Obnovit heslo", + "encryptionKeys": "Šifrovací klíče", + "weakStrength": "Slabé", + "strongStrength": "Silné", + "passwordChangedSuccessfully": "Heslo úspěšně změněno", + "generatingEncryptionKeys": "Generování šifrovacích klíčů...", "pleaseWait": "Čekejte prosím...", "continueLabel": "Pokračovat", + "howItWorks": "Jak to funguje", "encryption": "Šifrování", + "logInLabel": "Přihlásit se", "changeEmail": "Změnit e-mail", "enterYourPassword": "Zadejte své heslo", "welcomeBack": "Vítejte zpět!", + "contactSupport": "Kontaktovat podporu", + "incorrectPasswordTitle": "Nesprávné heslo", "pleaseTryAgain": "Zkuste to prosím znovu", "saveKey": "Uložit klíč", "confirm": "Potvrdit", "lostDevice": "Ztratili jste zařízení?", + "verifyingRecoveryKey": "Ověřování obnovovacího klíče...", + "recoveryKeyVerified": "Obnovovací klíč byl ověřen", "invalidKey": "Neplatný klíč", + "tryAgain": "Zkusit znovu", "albumOwner": "Vlastník", "@albumOwner": { "description": "Role of the album owner" }, "you": "Vy", "collaborator": "Spolupracovník", + "addMore": "Přidat další", + "@addMore": { + "description": "Button text to add more collaborators/viewers" + }, "viewer": "Prohlížející", "remove": "Odstranit", + "removeParticipant": "Odebrat účastníka", + "@removeParticipant": { + "description": "menuSectionTitle for removing a participant" + }, "manage": "Spravovat", "linkNeverExpires": "Nikdy", + "setAPassword": "Nastavit heslo", + "lockButtonLabel": "Uzamknout", + "enterPassword": "Zadejte heslo", "removeLink": "Odstranit odkaz", + "linkExpiresOn": "Platnost odkazu vyprší {expiryTime}", + "albumUpdated": "Album bylo aktualizováno", "never": "Nikdy", "after1Hour": "Po 1 hodině", "after1Day": "Po 1 dni", "after1Week": "Po 1 týdnu", "after1Month": "Po 1 měsíci", "after1Year": "Po 1 roce", + "manageParticipants": "Spravovat", "sendLink": "Odeslat odkaz", "linkHasExpired": "Platnost odkazu vypršela", + "verifyEmailID": "Ověřit {email}", + "sendInvite": "Odeslat pozvánku", "done": "Hotovo", "apply": "Použít", + "codeAppliedPageTitle": "Kód byl použit", "faq": "Často kladené dotazy", + "oopsSomethingWentWrong": "Jejda, něco se pokazilo", + "removeFromAlbumTitle": "Odstranit z alba?", + "removeFromAlbum": "Odstranit z alba", + "subscribe": "Odebírat", + "deleteSharedAlbum": "Opustit sdílené album?", + "deleteAlbum": "Odstranit album", "yesRemove": "Ano, odstranit", + "removeWithQuestionMark": "Odstranit?", + "inviteToEnte": "Pozvat do Ente", + "removePublicLink": "Odstranit veřejný odkaz", + "sharing": "Sdílení...", + "importing": "Importování…", + "failedToLoadAlbums": "Nepodařilo se načíst alba", + "hidden": "Skryté", "trash": "Koš", + "deleteFromEnte": "Odstranit z Ente", "yesDelete": "Ano, smazat", "movedToTrash": "Přesunuto do koše", + "deleteFromDevice": "Odstranit ze zařízení", + "deleteFromBoth": "Odstranit z obou", "newAlbum": "Nové album", + "albums": "Alba", "advancedSettings": "Pokročilé", "@advancedSettings": { "description": "The text to display in the advanced settings section" }, + "discover_screenshots": "Snímky obrazovky", "discover_pets": "Domácí mazlíčci", "discover_selfies": "Selfie", "discover_wallpapers": "Pozadí", "discover_food": "Jídlo", + "clearIndexes": "Smazat indexy", + "selectFoldersForBackup": "Vyberte složky pro zálohování", + "unselectAll": "Zrušit výběr", + "selectAll": "Vybrat vše", "skip": "Přeskočit", + "backupStatus": "Stav zálohování", + "weAreOpenSource": "Jsme open source!", "privacy": "Soukromí", + "terms": "Podmínky", + "checkForUpdates": "Zkontrolovat aktualizace", + "checkStatus": "Zkontrolovat stav", + "checking": "Probíhá kontrola...", "account": "Účet", "manageSubscription": "Spravovat předplatné", + "changePassword": "Změnit heslo", + "exportYourData": "Exportujte svá data", "logout": "Odhlásit se", + "areYouSureYouWantToLogout": "Opravdu se chcete odhlásit?", + "yesLogout": "Ano, odhlásit se", + "aNewVersionOfEnteIsAvailable": "Je dostupná nová verze Ente.", + "update": "Aktualizovat", + "installManually": "Instalovat manuálně", + "updateAvailable": "Je k dispozici aktualizace", + "ignoreUpdate": "Ignorovat", + "downloading": "Stahuji...", + "cannotDeleteSharedFiles": "Sdílené soubory nelze odstranit", + "theDownloadCouldNotBeCompleted": "Stahování nebylo možné dokončit", + "retry": "Opakovat", + "backedUpFolders": "Zálohované složky", + "backup": "Zálohovat", "removeDuplicates": "Odstranit duplicity", "noDuplicates": "✨ Žádné duplicity", "rateUs": "Ohodnoť nás", @@ -78,11 +175,27 @@ "security": "Zabezpečení", "no": "Ne", "yes": "Ano", + "rateUsOnStore": "Ohodnoťte nás na {storeName}", "blog": "Blog", + "merchandise": "E-shop", "twitter": "Twitter", "mastodon": "Mastodon", "matrix": "Matrix", "discord": "Discord", + "reddit": "Reddit", + "reportABug": "Nahlásit chybu", + "reportBug": "Nahlásit chybu", + "suggestFeatures": "Navrhnout funkce", + "support": "Podpora", + "theme": "Motiv", + "lightTheme": "Světlý", + "darkTheme": "Tmavý", + "systemTheme": "Systém", + "selectYourPlan": "Vyberte svůj plán", + "faqs": "Často kladené dotazy", + "renewsOn": "Předplatné se obnoví {endDate}", + "subscription": "Předplatné", + "paymentDetails": "Platební údaje", "yesRenew": "Ano, obnovit", "yesCancel": "Ano, zrušit", "monthly": "Měsíčně", @@ -95,19 +208,38 @@ "description": "The text to display for yearly plans", "type": "text" }, + "send": "Odeslat", "appleId": "Apple ID", "thankYou": "Děkujeme", "leave": "Odejít", + "openSettings": "Otevřít Nastavení", + "safelyStored": "Bezpečně uloženo", + "everywhere": "všude", + "pleaseLoginAgain": "Přihlaste se, prosím, znovu", "upgrade": "Upgradovat", + "sessionExpired": "Relace vypršela", "loggingOut": "Odhlašování...", + "@onDevice": { + "description": "The text displayed above folders/albums stored on device", + "type": "text" + }, + "onDevice": "V zařízení", + "@onEnte": { + "description": "The text displayed above albums backed up to Ente", + "type": "text" + }, + "onEnte": "Na ente", "newest": "Nejnovější", "deleteEmptyAlbums": "Smazat prázdná alba", "deleteEmptyAlbumsWithQuestionMark": "Smazat prázdná alba?", + "permanentlyDelete": "Trvale odstranit", + "publicLinkCreated": "Veřejný odkaz vytvořen", "restore": "Obnovit", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", "type": "text" }, + "moveToAlbum": "Přesunout do alba", "favorite": "Oblíbené", "shareLink": "Sdílet odkaz", "collageLayout": "Rozvržení", @@ -115,34 +247,62 @@ "delete": "Smazat", "hide": "Skrýt", "share": "Sdílet", + "selectAlbum": "Vybrat album", + "searchByAlbumNameHint": "Název alba", + "enterAlbumName": "Zadejte název alba", + "restoringFiles": "Obnovuji soubory...", + "uploadingFilesToAlbum": "Nahrávání souborů do alba...", + "addedSuccessfullyTo": "Úspěšně přidáno do {albumName}", "invite": "Pozvat", "sharedWithMe": "Sdíleno se mnou", "sharedByMe": "Sdíleno mnou", + "doubleYourStorage": "Zdvojnásobte své úložiště", + "deleteAll": "Smazat vše", "renameAlbum": "Přejmenovat album", "sortAlbumsBy": "Seřadit podle", "sortNewestFirst": "Od nejnovějších", "sortOldestFirst": "Od nejstarších", + "rename": "Přejmenovat", "leaveAlbum": "Opustit album", "archiveAlbum": "Archivovat album", + "calculating": "Probíhá výpočet...", "noResultsFound": "Nebyly nalezeny žádné výsledky", "exif": "EXIF", "close": "Zavřít", "download": "Stáhnout", + "downloadFailed": "Stahování selhalo", + "deselectAll": "Zrušte výběr všech", + "count": "Počet", "totalSize": "Celková velikost", + "decryptingVideo": "Dešifrování videa...", "unlock": "Odemknout", + "freeUpSpace": "Uvolnit místo", "verifying": "Ověřování...", "loadingGallery": "Načítání galerie...", "syncing": "Synchronizace...", + "syncStopped": "Synchronizace zastavena", "archiving": "Archivování...", "renameFile": "Přejmenovat soubor", + "enterFileName": "Zadejte název souboru", + "filesDeleted": "Soubory odstraněny", + "emptyTrash": "Vyprázdnit koš?", "empty": "Vyprázdnit", + "cachedData": "Data uložená v mezipaměti", + "clearCaches": "Vymazat mezipaměť", + "exportLogs": "Exportovat logy", "dismiss": "Zrušit", + "didYouKnow": "Věděli jste?", "location": "Poloha", + "moments": "Momenty", "searchAlbumsEmptySection": "Alba", "language": "Jazyk", "selectLanguage": "Vybrat jazyk", + "addLocation": "Přidat polohu", + "addLocationButton": "Přidat", + "radius": "Rádius", "save": "Uložit", "edit": "Upravit", + "deleteLocation": "Odstranit polohu", "rotateLeft": "Otočit doleva", "flip": "Překlopit", "rotateRight": "Otočit doprava", @@ -155,15 +315,29 @@ "usedSpace": "Využité místo", "storageBreakupFamily": "Rodina", "verifyIDLabel": "Ověřit", + "fileInfoAddDescHint": "Přidat popis...", + "editLocationTagTitle": "Upravit lokalitu", "setLabel": "Nastavit", "@setLabel": { "description": "Label of confirm button to add a new custom radius to the radius selector of a location tag" }, "familyPlanPortalTitle": "Rodina", + "androidBiometricSuccess": "Úspěšně dokončeno", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, "androidCancelButton": "Zrušit", "@androidCancelButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." }, + "androidBiometricRequiredTitle": "Je požadováno biometrické ověření", + "@androidBiometricRequiredTitle": { + "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters." + }, + "goToSettings": "Jít do nastavení", + "@goToSettings": { + "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." + }, "iOSOkButton": "OK", "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." @@ -173,6 +347,8 @@ "description": "Label for the map view" }, "maps": "Mapy", + "addPhotos": "Přidat fotky", + "noPhotosFoundHere": "Zde nebyly nalezeny žádné fotky", "unpinAlbum": "Odepnout album", "pinAlbum": "Připnout album", "create": "Vytvořit", @@ -182,7 +358,10 @@ "sharedByYou": "Sdíleno vámi", "inviteYourFriendsToEnte": "Pozvěte své přátelé do Ente", "failedToDownloadVideo": "Stahování videa se nezdařilo", + "crashReporting": "Hlášení o pádu", + "fileTypes": "Typy souboru", "yourMap": "Vaše mapa", + "photos": "Fotky", "videos": "Videa", "faces": "Obličeje", "people": "Lidé", @@ -192,44 +371,70 @@ }, "contacts": "Kontakty", "noInternetConnection": "Žádné připojení k internetu", + "editLocation": "Upravit polohu", + "selectALocation": "Vybrat polohu", + "selectALocationFirst": "Nejprve vyberte polohu", + "loginWithTOTP": "Přihlášení pomocí TOTP", "loginSessionExpired": "Relace vypršela", + "pair": "Spárovat", + "deviceNotFound": "Zařízení nebylo nalezeno", "deviceCodeHint": "Zadejte kód", "locations": "Lokality", + "search": "Hledat", "savePerson": "Uložit osobu", "editPerson": "Upravit osobu", + "mergedPhotos": "Sloučené fotografie", "enterDateOfBirth": "Narozeniny (volitelné)", "birthday": "Narozeniny", + "stopCastingTitle": "Zastavit přenos", "crop": "Oříznout", "rotate": "Otočit", "left": "Doleva", "right": "Doprava", "whatsNew": "Co je nového", "enable": "Povolit", + "enabled": "Zapnuto", "moreDetails": "Další podrobnosti", "panorama": "Panorama", + "reenterPin": "Zadejte PIN znovu", + "deviceLock": "Zámek zařízení", "next": "Další", "setNewPassword": "Nastavit nové heslo", + "enterPin": "Zadejte PIN", + "setNewPin": "Nastavit nový PIN", "autoLock": "Automatické zamykání", "immediately": "Ihned", "sort": "Seřadit", "personName": "Jméno osoby", "addNewPerson": "Přidat novou osobu", + "newPerson": "Nová osoba", "addName": "Přidat název", "add": "Přidat", + "configuration": "Nastavení", "resetPerson": "Odstranit", + "failedToPlayVideo": "Přehrávání videa se nezdařilo", "info": "Informace", + "addFiles": "Přidat soubory", "openFile": "Otevřít soubor", + "backupFile": "Zálohovat soubor", + "openAlbumInBrowser": "Otevřít album v prohlížeči", "allow": "Povolit", "acceptTrustInvite": "Přijmout pozvání", "declineTrustInvite": "Odmítnout pozvání", "removeInvite": "Odstranit pozvání", + "cancelAccountRecovery": "Zrušit obnovení", + "warning": "Varování", "gallery": "Galerie", + "join": "Připojit se", "me": "Já", "thisIsMeExclamation": "To jsem já!", + "processing": "Zpracovává se", + "queued": "Ve frontě", "editTime": "Upravit čas", "selectTime": "Vybrat čas", "selectDate": "Vybrat datum", "previous": "Předchozí", - "cLIcon": "Nová ikona", - "cLMemories": "Vzpomínky" + "newRange": "Nový rozsah", + "youAndThem": "Vy a {name}", + "selfiesWithThem": "Selfie s {name}" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_da.arb b/mobile/lib/l10n/intl_da.arb index 7870608a78..dfd9dee37f 100644 --- a/mobile/lib/l10n/intl_da.arb +++ b/mobile/lib/l10n/intl_da.arb @@ -207,7 +207,7 @@ "after1Month": "Efter 1 måned", "after1Year": "Efter 1 år", "manageParticipants": "Administrer", - "albumParticipantsCount": "{count, plural, one {}=0 {Ingen Deltagere} =1 {1 Deltager} other {{count} Deltagere}}", + "albumParticipantsCount": "{count, plural, =0 {Ingen Deltagere} =1 {1 Deltager} other {{count} Deltagere}}", "@albumParticipantsCount": { "placeholders": { "count": { diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index c2afa5f556..9f74523ca4 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Gib deine E-Mail-Adresse ein", + "enterYourNewEmailAddress": "Gib Deine neue E-Mail-Adresse ein", "accountWelcomeBack": "Willkommen zurück!", "emailAlreadyRegistered": "E-Mail ist bereits registriert.", "emailNotRegistered": "E-Mail nicht registriert.", @@ -721,6 +722,7 @@ "type": "text" }, "backupFailed": "Sicherung fehlgeschlagen", + "sorryBackupFailedDesc": "Leider konnten wir diese Datei momentan nicht sichern, wir werden es später erneut versuchen.", "couldNotBackUpTryLater": "Deine Daten konnten nicht gesichert werden.\nWir versuchen es später erneut.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kann Dateien nur verschlüsseln und sichern, wenn du den Zugriff darauf gewährst", "pleaseGrantPermissions": "Bitte erteile die nötigen Berechtigungen", @@ -1029,7 +1031,7 @@ "didYouKnow": "Schon gewusst?", "loadingMessage": "Fotos werden geladen...", "loadMessage1": "Du kannst dein Abonnement mit deiner Familie teilen", - "loadMessage2": "Wir haben bereits mehr als 30 Millionen Erinnerungsstücke gesichert", + "loadMessage2": "Wir haben bereits über 200 Millionen Erinnerungen bewahrt", "loadMessage3": "Wir behalten 3 Kopien Ihrer Daten, eine in einem unterirdischen Schutzbunker", "loadMessage4": "Alle unsere Apps sind Open-Source", "loadMessage5": "Unser Quellcode und unsere Kryptografie wurden extern geprüft", @@ -1280,6 +1282,8 @@ "createCollaborativeLink": "Gemeinschaftlichen Link erstellen", "search": "Suche", "enterPersonName": "Namen der Person eingeben", + "editEmailAlreadyLinked": "Diese E-Mail ist bereits verknüpft mit {name}.", + "viewPersonToUnlink": "{name} zum Entfernen des Links anzeigen", "enterName": "Name eingeben", "savePerson": "Person speichern", "editPerson": "Person bearbeiten", @@ -1410,7 +1414,7 @@ }, "description": "Number of viewers that were successfully added to an album." }, - "collaboratorsSuccessfullyAdded": "{count, plural, one {}=0 {0 Mitarbeiter hinzugefügt} =1 {1 Mitarbeiter hinzugefügt} other {{count} Mitarbeiter hinzugefügt}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 Mitarbeiter hinzugefügt} =1 {1 Mitarbeiter hinzugefügt} other {{count} Mitarbeiter hinzugefügt}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1659,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video-Streaming", + "videoStreaming": "Streambare Videos", "processingVideos": "Verarbeite Videos", "streamDetails": "Stream-Details", "processing": "In Bearbeitung", @@ -1726,15 +1730,31 @@ "onTheRoad": "Wieder unterwegs", "food": "Kulinarische Genüsse", "pets": "Pelzige Begleiter", - "cLIcon": "Neues Icon", - "cLIconDesc": "Endlich ein neues App-Icon, das unserer Meinung nach unser Werk am besten repräsentiert. Zudem ist es möglich, weiterhin das alte App-Icon zu verwenden.", - "cLMemories": "Erinnerungen", - "cLMemoriesDesc": "Entdecke Deine besonderen Momente neu – Spot auf Deine liebsten Personen, Deine Reisen und Urlaube, Deine besten Schnappschüsse und vieles mehr. Aktiviere das maschinelle Lernen, tagge Dich selbst und benenne Deine Freunde für die besten Ergebnisse.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Homescreen-Widgets mit integrierten Erinnerungen sind nun verfügbar. Sie zeigen dir deine besonderen Momente an, ohne die App zu öffnen.", - "cLFamilyPlan": "Obergrenzen für den Familientarif", - "cLFamilyPlanDesc": "Du kannst jetzt festlegen, wie viel Speicherplatz deine Familienmitglieder nutzen können.", - "cLBulkEdit": "Massenbearbeitung von Datumsangaben", - "cLBulkEditDesc": "Du kannst jetzt mehrere Fotos auswählen, und das Datum/Uhrzeit für alle mit einer Aktion ändern. Das Verschieben von Daten wird auch unterstützt.", - "curatedMemories": "Ausgewählte Erinnerungen" + "curatedMemories": "Ausgewählte Erinnerungen", + "widgets": "Widgets", + "memories": "Erinnerungen", + "peopleWidgetDesc": "Wähle die Personen, die du auf der Startseite sehen möchtest.", + "albumsWidgetDesc": "Wähle die Alben, die du auf der Startseite sehen möchtest.", + "memoriesWidgetDesc": "Wähle die Arten von Erinnerungen, die du auf der Startseite sehen möchtest.", + "smartMemories": "Smarte Erinnerungen", + "pastYearsMemories": "Erinnerungen der letzten Jahre", + "deleteMultipleAlbumDialog": "Sollen die Fotos (und Videos) aus diesen {count} Alben auch aus allen anderen Alben gelöscht werden, in denen sie enthalten sind?", + "addParticipants": "Teilnehmer hinzufügen", + "selectedAlbums": "{count} ausgewählt", + "actionNotSupportedOnFavouritesAlbum": "Aktion für das Favoritenalbum nicht unterstützt", + "onThisDayMemories": "Erinnerungen an diesem Tag", + "onThisDay": "An diesem Tag", + "lookBackOnYourMemories": "Schau zurück auf deine Erinnerungen 🌄", + "newPhotosEmoji": " neue 📸", + "sorryWeHadToPauseYourBackups": "Entschuldigung, wir mussten deine Sicherungen pausieren", + "clickToInstallOurBestVersionYet": "Klicke, um unsere bisher beste Version zu installieren", + "onThisDayNotificationExplanation": "Erhalte Erinnerungen von diesem Tag in den vergangenen Jahren.", + "addMemoriesWidgetPrompt": "Füge ein Erinnerungs-Widget zu deiner Startseite hinzu und komm hierher zurück, um es anzupassen.", + "addAlbumWidgetPrompt": "Füge ein Alben-Widget zu deiner Startseite hinzu und komm hierher zurück, um es anzupassen.", + "addPeopleWidgetPrompt": "Füge ein Personen-Widget zu deiner Startseite hinzu und komm hierher zurück, um es anzupassen.", + "birthdayNotifications": "Geburtstagsbenachrichtigungen", + "receiveRemindersOnBirthdays": "Erhalte Erinnerungen, wenn jemand Geburtstag hat. Ein Klick auf die Benachrichtigung bringt dich zu den Fotos der Person, die Geburtstag hat.", + "happyBirthday": "Herzlichen Glückwunsch zum Geburtstag! 🥳", + "happyBirthdayToPerson": "Alles Gutes zum Geburtstag an {name}! 🎉", + "birthdays": "Geburtstage" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index d01df436c2..57d3db6577 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Enter your email address", + "enterYourNewEmailAddress": "Enter your new email address", "accountWelcomeBack": "Welcome back!", "emailAlreadyRegistered": "Email already registered.", "emailNotRegistered": "Email not registered.", @@ -721,6 +722,7 @@ "type": "text" }, "backupFailed": "Backup failed", + "sorryBackupFailedDesc": "Sorry, we could not backup this file right now, we will retry later.", "couldNotBackUpTryLater": "We could not backup your data.\nWe will retry later.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente can encrypt and preserve files only if you grant access to them", "pleaseGrantPermissions": "Please grant permissions", @@ -1029,7 +1031,7 @@ "didYouKnow": "Did you know?", "loadingMessage": "Loading your photos...", "loadMessage1": "You can share your subscription with your family", - "loadMessage2": "We have preserved over 30 million memories so far", + "loadMessage2": "We have preserved over 200 million memories so far", "loadMessage3": "We keep 3 copies of your data, one in an underground fallout shelter", "loadMessage4": "All our apps are open source", "loadMessage5": "Our source code and cryptography have been externally audited", @@ -1280,6 +1282,8 @@ "createCollaborativeLink": "Create collaborative link", "search": "Search", "enterPersonName": "Enter person name", + "editEmailAlreadyLinked": "This email is already linked to {name}.", + "viewPersonToUnlink": "View {name} to unlink", "enterName": "Enter name", "savePerson": "Save person", "editPerson": "Edit person", @@ -1659,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video streaming", + "videoStreaming": "Streamable videos", "processingVideos": "Processing videos", "streamDetails": "Stream details", "processing": "Processing", @@ -1726,15 +1730,31 @@ "onTheRoad": "On the road again", "food": "Culinary delight", "pets": "Furry companions", - "cLIcon": "New Icon", - "cLIconDesc": "Finally, a new app icon, that we think best represents our work. We've also added an icon-switcher so you can continue using the old icon.", - "cLMemories": "Memories", - "cLMemoriesDesc": "Rediscover your special moments - spotlight on your favorite people, your trips and holidays, your best clicks, and much more. Turn on machine learning, tag yourself and name your friends for the best experience.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Home screen widgets that are integrated with memories are now available. They will show your special moments without opening the app.", - "cLFamilyPlan": "Family Plan Limits", - "cLFamilyPlanDesc": "You can now set limits on how much storage your family members can use.", - "cLBulkEdit": "Bulk Edit dates", - "cLBulkEditDesc": "You can now select multiple photos, and edit date/time for all of them with one quick action. Shifting dates is also supported.", - "curatedMemories": "Curated memories" -} \ No newline at end of file + "curatedMemories": "Curated memories", + "widgets": "Widgets", + "memories": "Memories", + "peopleWidgetDesc": "Select the people you wish to see on your homescreen.", + "albumsWidgetDesc": "Select the albums you wish to see on your homescreen.", + "memoriesWidgetDesc": "Select the kind of memories you wish to see on your homescreen.", + "smartMemories": "Smart memories", + "pastYearsMemories": "Past years' memories", + "deleteMultipleAlbumDialog": "Also delete the photos (and videos) present in these {count} albums from all other albums they are part of?", + "addParticipants": "Add participants", + "selectedAlbums": "{count} selected", + "actionNotSupportedOnFavouritesAlbum": "Action not supported on Favourites album", + "onThisDayMemories": "On this day memories", + "onThisDay": "On this day", + "lookBackOnYourMemories": "Look back on your memories 🌄", + "newPhotosEmoji": " new 📸", + "sorryWeHadToPauseYourBackups": "Sorry, we had to pause your backups", + "clickToInstallOurBestVersionYet": "Click to install our best version yet", + "onThisDayNotificationExplanation": "Receive reminders about memories from this day in previous years.", + "addMemoriesWidgetPrompt": "Add a memories widget to your homescreen and come back here to customize.", + "addAlbumWidgetPrompt": "Add an album widget to your homescreen and come back here to customize.", + "addPeopleWidgetPrompt": "Add a people widget to your homescreen and come back here to customize.", + "birthdayNotifications": "Birthday notifications", + "receiveRemindersOnBirthdays": "Receive reminders when it's someone's birthday. Tapping on the notification will take you to photos of the birthday person.", + "happyBirthday": "Happy birthday! 🥳", + "happyBirthdayToPerson": "Happy birthday to {name}! 🎉", + "birthdays": "Birthdays" +} diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index 412e447423..665724372d 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -371,6 +371,21 @@ "deleteFromBoth": "Eliminar de ambos", "newAlbum": "Nuevo álbum", "albums": "Álbumes", + "memoryCount": "{count, plural, =0{no hay recuerdos} one{{formattedCount} recuerdo} other{{formattedCount} recuerdos}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} seleccionados", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -777,6 +792,14 @@ "share": "Compartir", "unhideToAlbum": "Hacer visible al álbum", "restoreToAlbum": "Restaurar al álbum", + "moveItem": "{count, plural,=1 {Mover objeto} other {Mover objetos}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Añadir objeto} other {Añadir objetos}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Crear o seleccionar álbum", "selectAlbum": "Seleccionar álbum", "searchByAlbumNameHint": "Nombre del álbum", @@ -874,6 +897,7 @@ "authToViewYourMemories": "Por favor, autentícate para ver tus recuerdos", "unlock": "Desbloquear", "freeUpSpace": "Liberar espacio", + "freeUpSpaceSaving": "{count, plural, =1 {Se puede eliminar del dispositivo para liberar {formattedSize}} other {Se pueden eliminar del dispositivo para liberar {formattedSize}}}", "filesBackedUpInAlbum": "Se ha realizado la copia de seguridad de {count, plural, one {1 archivo} other {{formattedNumber} archivos}} de este álbum de forma segura", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +928,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Aún puedes acceder {count, plural,=1 {a él} other {a ellos}} en Ente mientras tengas una suscripción activa", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "Liberar {sizeInMBorGB}", "thisEmailIsAlreadyInUse": "Este correo electrónico ya está en uso", "incorrectCode": "Código incorrecto", @@ -993,7 +1029,6 @@ "didYouKnow": "¿Sabías que?", "loadingMessage": "Cargando tus fotos...", "loadMessage1": "Puedes compartir tu suscripción con tu familia", - "loadMessage2": "Hasta ahora hemos conservado más de 30 millones de recuerdos", "loadMessage3": "Guardamos 3 copias de tus datos, una en un refugio subterráneo", "loadMessage4": "Todas nuestras aplicaciones son de código abierto", "loadMessage5": "Nuestro código fuente y criptografía han sido auditados externamente", @@ -1231,6 +1266,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Encuentra gente rápidamente por su nombre", + "addViewers": "{count, plural, =0 {Añadir espectador} =1{Añadir espectador} other {Añadir espectadores}}", + "addCollaborators": "{count, plural, =0 {Añadir colaborador} =1 {Añadir colaborador} other {Añadir colaboradores}}", "longPressAnEmailToVerifyEndToEndEncryption": "Mantén pulsado un correo electrónico para verificar el cifrado de extremo a extremo.", "developerSettingsWarning": "¿Estás seguro de que quieres modificar los ajustes de desarrollador?", "developerSettings": "Ajustes de desarrollador", @@ -1362,6 +1399,16 @@ "enableMachineLearningBanner": "Activar aprendizaje automático para búsqueda mágica y reconocimiento facial", "searchDiscoverEmptySection": "Las imágenes se mostrarán aquí cuando se complete el procesado y la sincronización", "searchPersonsEmptySection": "Las personas se mostrarán aquí cuando se complete el procesado y la sincronización", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 espectadores añadidos} =1 {1 espectador añadido} other {{count} espectadores añadidos}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 colaboradores añadidos} =1 {1 colaborador añadido} other {{count} colaboradores añadidos}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1437,6 +1484,15 @@ }, "currentlyRunning": "ejecutando", "ignored": "ignorado", + "photosCount": "{count, plural, =0 {0 fotos} =1 {1 foto} other {{count} fotos}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Archivo", "searchSectionsLengthMismatch": "La longitud de las secciones no coincide: {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1602,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Transmisión de vídeo", "processingVideos": "Procesando vídeos", "streamDetails": "Detalles de la transmisión", "processing": "Procesando", @@ -1617,17 +1672,30 @@ "selectTime": "Seleccionar hora", "selectDate": "Seleccionar fecha", "previous": "Anterior", + "selectOneDateAndTimeForAll": "Seleccione una fecha y hora para todas", "selectStartOfRange": "Seleccionar inicio del rango", "thisWillMakeTheDateAndTimeOfAllSelected": "Esto hará que la fecha y la hora de todas las fotos seleccionadas sean las mismas.", + "allWillShiftRangeBasedOnFirst": "Este es el primero en el grupo. Otras fotos seleccionadas cambiarán automáticamente basándose en esta nueva fecha", "newRange": "Nuevo rango", "selectOneDateAndTime": "Seleccionar fecha y hora", "moveSelectedPhotosToOneDate": "Mover las fotos seleccionadas a una fecha", "shiftDatesAndTime": "Cambiar fechas y hora", + "photosKeepRelativeTimeDifference": "Las fotos mantienen una diferencia de tiempo relativa", + "photocountPhotos": "{count, plural, =0 {No hay fotos} =1 {1 foto} other {{count} fotos}}", + "@photocountPhotos": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "appIcon": "Ícono", "notThisPerson": "¿No es esta persona?", "selectedItemsWillBeRemovedFromThisPerson": "Los elementos seleccionados se eliminarán de esta persona, pero no se eliminarán de tu biblioteca.", "throughTheYears": "{dateFormat} a través de los años", "thisWeekThroughTheYears": "Esta semana a través de los años", + "thisWeekXYearsAgo": "{count, plural, =1 {Esta semana, hace {count} año} other {Esta semana, hace {count} años}}", "youAndThem": "Tú y {name}", "admiringThem": "Admirando a {name}", "embracingThem": "Abrazando a {name}", @@ -1639,6 +1707,9 @@ "backgroundWithThem": "Preciosas vistas con {name}", "sportsWithThem": "Deportes con {name}", "roadtripWithThem": "Viaje en carretera con {name}", + "spotlightOnYourself": "Enfócate a ti mismo", + "spotlightOnThem": "Enfocar a {name}", + "personIsAge": "¡{name} tiene {age} años!", "personTurningAge": "{name} cumpliendo {age} pronto", "lastTimeWithThem": "Última vez con {name}", "tripToLocation": "Viaje a {location}", @@ -1653,12 +1724,5 @@ "onTheRoad": "De nuevo en la carretera", "food": "Delicia culinaria", "pets": "Compañeros peludos", - "cLIcon": "Nuevo ícono", - "cLIconDesc": "Por fin, un nuevo icono de la aplicación, que creemos que representa mejor nuestro trabajo. También hemos añadido una opción para que puedas seguir utilizando el icono anterior.", - "cLMemories": "Recuerdos", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Ya están disponibles los widgets de pantalla de inicio con tus recuerdos. Podrás ver tus momentos especiales sin abrir la aplicación.", - "cLFamilyPlan": "Límites de plan familiar", - "cLFamilyPlanDesc": "Ahora puede establecer límites en cuanto al almacenamiento que los miembros de tu familia pueden utilizar.", - "cLBulkEditDesc": "Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas." + "curatedMemories": "Memorias revisadas" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_eu.arb b/mobile/lib/l10n/intl_eu.arb index 1e05802489..b08aaee4ef 100644 --- a/mobile/lib/l10n/intl_eu.arb +++ b/mobile/lib/l10n/intl_eu.arb @@ -290,5 +290,173 @@ "details": "Detaileak", "claimMore": "Eskatu gehiago!", "theyAlsoGetXGb": "Haiek ere lortuko dute {storageAmountInGB} GB", - "freeStorageOnReferralSuccess": "{storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu" + "freeStorageOnReferralSuccess": "{storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu", + "shareTextReferralCode": "Sartu erreferentzia kodea: {referralCode}\n\nAplikatu hemen: Ezarpenak → Orokorra→ Erreferentziak, {referralStorageInGB} GB dohainik izateko ordainpeko plan batean \n\nhttps://ente.io", + "claimFreeStorage": "Eskatu debaldeko biltegiratzea", + "inviteYourFriends": "Gonbidatu zure lagunak", + "failedToFetchReferralDetails": "Ezin dugu zure erreferentziaren detailerik lortu. Mesedez, saiatu berriro geroago.", + "referralStep1": "1. Eman kode hau zure lagunei", + "referralStep2": "2. Haiek ordainpeko plan batean sinatu behar dute", + "referralStep3": "3. Bai zuk bai haiek {storageInGB} GB* dohainik izango duzue", + "referralsAreCurrentlyPaused": "Erreferentziak momentuz geldituta daude", + "youCanAtMaxDoubleYourStorage": "* Gehienez zure biltegiratzea bikoiztu ahal duzu", + "claimedStorageSoFar": "{isFamilyMember, select,true {Zure familiak {storageAmountInGb} GB eskatu du dagoeneko} false {Zuk {storageAmountInGb} GB eskatu duzu dagoeneko} other {Zuk {storageAmountInGb} GB eskatu duzu dagoeneko!}}", + "@claimedStorageSoFar": { + "placeholders": { + "isFamilyMember": { + "type": "String", + "example": "true" + }, + "storageAmountInGb": { + "type": "int", + "example": "10" + } + } + }, + "faq": "FAQ", + "help": "Laguntza", + "oopsSomethingWentWrong": "Oops, zerbait txarto joan da", + "peopleUsingYourCode": "Jendea zure kodea erabiltzen", + "eligible": "aukerakoak", + "total": "osotara", + "codeUsedByYou": "Zuk erabilitako kodea", + "freeStorageClaimed": "Debaldeko biltegiratzea eskatuta", + "freeStorageUsable": "Debaldeko biltegiratzea erabilgarri", + "usableReferralStorageInfo": "Biltegiratze erabilgarria zure oraingo planaren arabera mugatuta dago. Soberan eskatutako biltegiratzea automatikoki erabili ahal izango duzu zure plan gaurkotzen duzunean.", + "removeFromAlbumTitle": "Albumetik kendu?", + "removeFromAlbum": "Kendu albumetik", + "itemsWillBeRemovedFromAlbum": "Hautatutako elementuak album honetatik kenduko dira", + "removeShareItemsWarning": "Kentzen ari zaren elementu batzuk beste pertsona batzuek gehitu zituzten, beraz ezin izango dituzu eskuratu", + "addingToFavorites": "Gogokoetan gehitzen...", + "removingFromFavorites": "Gogokoetatik kentzen...", + "sorryCouldNotAddToFavorites": "Sentitzen dut, ezin izan dugu zure gogokoetan gehitu!", + "sorryCouldNotRemoveFromFavorites": "Sentitzen dugu, ezin izan dugu zure gogokoetatik kendu!", + "subscribeToEnableSharing": "Ordainpeko harpidetza behar duzu partekatzea aktibatzeko.", + "subscribe": "Harpidetu", + "canOnlyRemoveFilesOwnedByYou": "Zure fitxategiak baino ezin duzu ezabatu", + "deleteSharedAlbum": "Partekatutako albuma ezabatu?", + "deleteAlbum": "Ezabatu albuma", + "deleteAlbumDialog": "Ezabatu nahi dituzu album honetan dauden argazkiak (eta bideoak) parte diren beste album guztietatik ere?", + "deleteSharedAlbumDialogBody": "Albuma guztiontzat ezabatuko da \n\nAlbum honetan dauden beste pertsonek partekatutako argazkiak ezin izango dituzu eskuratu", + "yesRemove": "Bai, ezabatu", + "creatingLink": "Esteka sortzen...", + "removeWithQuestionMark": "Ezabatuko?", + "removeParticipantBody": "{userEmail} partekatutako album honetatik ezabatuko da \n\nHaiek gehitutako argazki guztiak ere ezabatuak izango dira albumetik", + "keepPhotos": "Gorde Argazkiak", + "deletePhotos": "Ezabatu argazkiak", + "inviteToEnte": "Gonbidatu Ente-ra", + "removePublicLink": "Ezabatu esteka publikoa", + "disableLinkMessage": "Honen bidez {albumName} eskuratzeko esteka publikoa ezabatuko da.", + "sharing": "Partekatzen...", + "youCannotShareWithYourself": "Ezin duzu zeure buruarekin partekatu", + "archive": "Artxiboa", + "createAlbumActionHint": "Luze klikatu argazkiak hautatzeko eta klikatu + albuma sortzeko", + "importing": "Inportatzen....", + "failedToLoadAlbums": "Errorea albumak kargatzen", + "hidden": "Ezkutatuta", + "authToViewYourHiddenFiles": "Mesedez, autentifikatu zure ezkutatutako fitxategiak ikusteko", + "authToViewTrashedFiles": "Mesedez, autentifikatu paperontzira botatako zure fitxategiak ikusteko", + "trash": "Zarama", + "uncategorized": "Kategori gabekoa", + "videoSmallCase": "bideoa", + "photoSmallCase": "argazkia", + "singleFileDeleteHighlight": "Album guztietatik ezabatuko da.", + "singleFileInBothLocalAndRemote": "{fileType} hau Ente-n eta zure gailuan dago.", + "singleFileInRemoteOnly": "{fileType} hau Ente-tik ezabatuko da.", + "singleFileDeleteFromDevice": "{fileType} hau zure gailutik ezabatuko da.", + "deleteFromEnte": "Ezabatu Ente-tik", + "yesDelete": "Bai, ezabatu", + "movedToTrash": "Zarama mugituta", + "deleteFromDevice": "Ezabatu gailutik", + "deleteFromBoth": "Ezabatu bietatik", + "newAlbum": "Album berria", + "albums": "Albumak", + "memoryCount": "{count, plural,=0{oroitzapenik ez}one{oroitzapen {formattedCount}} other{{formattedCount} oroitzapen}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, + "selectedPhotos": "{count} hautatuta", + "@selectedPhotos": { + "description": "Display the number of selected photos", + "type": "text", + "placeholders": { + "count": { + "example": "5", + "type": "int" + } + } + }, + "selectedPhotosWithYours": "{count} hautatuta ({yourCount} zureak)", + "@selectedPhotosWithYours": { + "description": "Display the number of selected photos, including the number of selected photos owned by the user", + "type": "text", + "placeholders": { + "count": { + "example": "12", + "type": "int" + }, + "yourCount": { + "example": "2", + "type": "int" + } + } + }, + "advancedSettings": "Aurreratuak", + "@advancedSettings": { + "description": "The text to display in the advanced settings section" + }, + "photoGridSize": "Argazki sarearen tamaina", + "manageDeviceStorage": "Kudeatu gailuaren katxea", + "manageDeviceStorageDesc": "Berrikusi eta garbitu katxe lokalaren biltegiratzea.", + "machineLearning": "Ikasketa automatikoa", + "mlConsent": "Aktibatu ikasketa automatikoa", + "mlConsentTitle": "Ikasketa automatikoa aktibatuko?", + "mlConsentDescription": "Ikasketa automatikoa aktibatuz gero, Ente-k fitxategietatik informazioa aterako du (ad. argazkien geometria), zurekin partekatutako argazkietatik ere.\n\nHau zure gailuan gertatuko da, eta sortutako informazio biometrikoa puntutik puntura zifratuta egongo da.", + "mlConsentPrivacy": "Mesedez, klikatu hemen gure pribatutasun politikan ezaugarri honi buruz detaile gehiago izateko", + "mlConsentConfirmation": "Ulertzen dut, eta ikasketa automatikoa aktibatu nahi dut", + "magicSearch": "Bilaketa magikoa", + "discover": "Aurkitu", + "@discover": { + "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." + }, + "discover_identity": "Nortasuna", + "discover_screenshots": "Pantaila argazkiak", + "discover_receipts": "Ordainagiriak", + "discover_notes": "Oharrak", + "discover_memes": "Memeak", + "discover_visiting_cards": "Bisita txartelak", + "discover_babies": "Umeak", + "discover_pets": "Etxe-animaliak", + "discover_selfies": "Selfiak", + "discover_wallpapers": "Horma-paperak", + "discover_food": "Janaria", + "discover_celebrations": "Ospakizunak", + "discover_sunset": "Eguzki-sartzea", + "discover_hills": "Muinoak", + "discover_greenery": "Hostoa", + "authToChangeYourEmail": "Mesedez, autentifikatu zure emaila aldatzeko", + "authToChangeYourPassword": "Mesedez, autentifikatu zure pasahitza aldatzeko", + "authToChangeEmailVerificationSetting": "Mesedez, autentifikatu emailaren egiaztatzea aldatzeko", + "authToInitiateAccountDeletion": "Mesedez, autentifikatu kontu ezabaketa hasteko", + "authToViewYourRecoveryKey": "Mesedez, autentifikatu zure berreskuratze giltza ikusteko", + "authToConfigureTwofactorAuthentication": "Mesedez, autentifikatu faktore biko autentifikazioa konfiguratzeko", + "authToChangeLockscreenSetting": "Mesedez, autentifikatu pantaila blokeatzeko ezarpenak aldatzeko", + "authToViewYourActiveSessions": "Mesedez, autentifikatu indarrean dauden zure saioak ikusteko", + "confirm2FADisable": "Seguru zaude faktore biko autentifikazioa deuseztatu nahi duzula?", + "twofactorAuthenticationHasBeenDisabled": "Faktore biko autentifikazioa deuseztatua izan da", + "iOSLockOut": "Autentifikazio biometrikoa deuseztatuta dago. Mesedez, blokeatu eta desblokeatu zure pantaila indarrean jartzeko.", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + } } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fa.arb b/mobile/lib/l10n/intl_fa.arb index ba9d203a8f..edabff8132 100644 --- a/mobile/lib/l10n/intl_fa.arb +++ b/mobile/lib/l10n/intl_fa.arb @@ -270,7 +270,6 @@ "tempErrorContactSupportIfPersists": "به نظر می‌رسد مشکلی وجود دارد. لطفا بعد از مدتی دوباره تلاش کنید. اگر همچنان با خطا مواجه می‌شوید، لطفا با تیم پشتیبانی ما ارتباط برقرار کنید.", "preparingLogs": "در حال آماده‌سازی لاگ‌ها...", "didYouKnow": "آیا می‌دانستید؟", - "loadMessage2": "ما تا کنون بیش از ۳۰ میلیون خاطره را حفظ کرده‌ایم", "color": "رنگ", "dayToday": "امروز", "dayYesterday": "دیروز", diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index f3c3d3c470..ab2e5b1553 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Entrez votre adresse e-mail", + "enterYourNewEmailAddress": "Entrez votre nouvelle adresse e-mail", "accountWelcomeBack": "Bon retour parmi nous !", "emailAlreadyRegistered": "Email déjà enregistré.", "emailNotRegistered": "E-mail non enregistré.", @@ -371,6 +372,21 @@ "deleteFromBoth": "Supprimer des deux", "newAlbum": "Nouvel album", "albums": "Albums", + "memoryCount": "{count, plural, =0{aucun souvenir} one{{formattedCount} souvenir} other{{formattedCount} souvenirs}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} sélectionné(s)", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -460,7 +476,7 @@ } } }, - "showMemories": "Montrer les souvenirs", + "showMemories": "Afficher les souvenirs", "yearsAgo": "{count, plural, one{il y a {count} an} other{il y a {count} ans}}", "backupSettings": "Paramètres de la sauvegarde", "backupStatus": "État de la sauvegarde", @@ -706,6 +722,7 @@ "type": "text" }, "backupFailed": "Échec de la sauvegarde", + "sorryBackupFailedDesc": "Désolé, nous n'avons pas pu sauvegarder ce fichier maintenant, nous allons réessayer plus tard.", "couldNotBackUpTryLater": "Nous n'avons pas pu sauvegarder vos données.\nNous allons réessayer plus tard.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente peut chiffrer et conserver des fichiers que si vous leur accordez l'accès", "pleaseGrantPermissions": "Veuillez accorder la permission", @@ -777,6 +794,14 @@ "share": "Partager", "unhideToAlbum": "Afficher dans l'album", "restoreToAlbum": "Restaurer vers l'album", + "moveItem": "{count, plural,=1 {Déplacer un élément} other {Déplacer des éléments}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Ajouter un élément} other {Ajouter des éléments}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Créez ou sélectionnez un album", "selectAlbum": "Sélectionner album", "searchByAlbumNameHint": "Nom de l'album", @@ -874,6 +899,7 @@ "authToViewYourMemories": "Authentifiez-vous pour voir vos souvenirs", "unlock": "Déverrouiller", "freeUpSpace": "Libérer de l'espace", + "freeUpSpaceSaving": "{count, plural, one {Il peut être supprimé de l'appareil pour libérer {formattedSize}} other {Ils peuvent être supprimés de l'appareil pour libérer {formattedSize}}}", "filesBackedUpInAlbum": "{count, plural, one {1 fichier dans cet album a été sauvegardé en toute sécurité} other {{formattedNumber} fichiers dans cet album ont été sauvegardés en toute sécurité}}", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +930,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Vous pouvez toujours {count, plural, one {l'} other {les}} accéder sur Ente tant que vous avez un abonnement actif", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "Libérer {sizeInMBorGB}", "thisEmailIsAlreadyInUse": "Cette adresse mail est déjà utilisé", "incorrectCode": "Code non valide", @@ -993,7 +1031,7 @@ "didYouKnow": "Le savais-tu ?", "loadingMessage": "Chargement de vos photos...", "loadMessage1": "Vous pouvez partager votre abonnement avec votre famille", - "loadMessage2": "Nous avons conservé plus de 30 millions de souvenirs jusqu'à présent", + "loadMessage2": "Nous avons préservé plus de 200 millions de souvenirs jusqu'à présent", "loadMessage3": "Nous conservons 3 copies de vos données, l'une dans un abri anti-atomique", "loadMessage4": "Toutes nos applications sont open source", "loadMessage5": "Notre code source et notre cryptographie ont été audités en externe", @@ -1231,6 +1269,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Trouver des personnes rapidement par leur nom", + "addViewers": "{count, plural, =0 {Ajouter un spectateur} =1 {Ajouter une spectateur} other {Ajouter des spectateurs}}", + "addCollaborators": "{count, plural, =0 {Ajouter un collaborateur} =1 {Ajouter un collaborateur} other {Ajouter des collaborateurs}}", "longPressAnEmailToVerifyEndToEndEncryption": "Appuyez longuement sur un email pour vérifier le chiffrement de bout en bout.", "developerSettingsWarning": "Êtes-vous sûr de vouloir modifier les paramètres du développeur ?", "developerSettings": "Paramètres du développeur", @@ -1242,6 +1282,8 @@ "createCollaborativeLink": "Créer un lien collaboratif", "search": "Rechercher", "enterPersonName": "Entrez le nom d'une personne", + "editEmailAlreadyLinked": "Cet e-mail est déjà lié à {name}.", + "viewPersonToUnlink": "Voir {name} pour délier", "enterName": "Saisir un nom", "savePerson": "Enregistrer la personne", "editPerson": "Modifier la personne", @@ -1362,6 +1404,16 @@ "enableMachineLearningBanner": "Activer l'apprentissage automatique pour la reconnaissance des visages et la recherche magique", "searchDiscoverEmptySection": "Les images seront affichées ici une fois le traitement terminé", "searchPersonsEmptySection": "Les personnes seront affichées ici une fois le traitement terminé", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 spectateur ajouté} =1 {Un spectateur ajouté} other {{count} spectateurs ajoutés}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 collaborateur ajouté} =1 {1 collaborateur ajouté} other {{count} collaborateurs ajoutés}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1437,6 +1489,15 @@ }, "currentlyRunning": "en cours d'exécution", "ignored": "ignoré", + "photosCount": "{count, plural, =0 {0 photo} =1 {1 photo} other {{count} photos}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Fichier", "searchSectionsLengthMismatch": "Incompatibilité de longueur des sections : {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1602,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Streaming vidéo", + "videoStreaming": "Vidéos diffusables", "processingVideos": "Traitement des vidéos", "streamDetails": "Détails du stream", "processing": "Traitement en cours", @@ -1640,7 +1701,7 @@ "selectedItemsWillBeRemovedFromThisPerson": "Les éléments sélectionnés seront retirés de cette personne, mais pas supprimés de votre bibliothèque.", "throughTheYears": "{dateFormat} au fil des années", "thisWeekThroughTheYears": "Cette semaine au fil des années", - "thisWeekXYearsAgo": "{count, plural, one {}=1 {Cette semaine, {count} il y a l'année} other {Cette semaine, {count} il y a des années}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Cette semaine, {count} il y a l'année} other {Cette semaine, {count} il y a des années}}", "youAndThem": "Vous et {name}", "admiringThem": "Admirant {name}", "embracingThem": "Embrasse {name}", @@ -1669,14 +1730,26 @@ "onTheRoad": "De nouveau sur la route", "food": "Plaisir culinaire", "pets": "Compagnons à quatre pattes", - "cLIcon": "Nouvel icône", - "cLIconDesc": "Finalement, création d'un nouvel icône d'application qui, selon nous, représente au mieux notre travail. Nous avons également ajouté un changeur d'icône pour que vous puissiez continuer à utiliser l'ancien.", - "cLMemories": "Souvenirs", - "cLMemoriesDesc": "Redécouvrez vos précieux souvenirs - focus sur vos connaissances préférées, vos voyages et vos vacances, vos meilleurs clics et bien plus encore. Activez l'apprentissage automatique, taguez-vous et nommez vos amis pour une meilleure expérience.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Les widgets (ou gadgets) de l'écran d'accueil, qui sont intégrés à des souvenirs, sont maintenant disponibles. Ils montreront vos moments spéciaux sans nécessité d'ouvrir l'application.", - "cLFamilyPlan": "Limites pour le forfait Famille", - "cLFamilyPlanDesc": "Vous pouvez maintenant fixer des limites sur la quantité de stockage que les membres de votre famille peuvent utiliser.", - "cLBulkEdit": "Dates de modification multiples", - "cLBulkEditDesc": "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge." + "curatedMemories": "Souvenirs conservés", + "widgets": "Gadgets", + "memories": "Souvenirs", + "peopleWidgetDesc": "Sélectionnez les personnes que vous souhaitez voir sur votre écran d'accueil.", + "albumsWidgetDesc": "Sélectionnez les personnes que vous souhaitez voir sur votre écran d'accueil.", + "memoriesWidgetDesc": "Sélectionnez le type de souvenirs que vous souhaitez voir sur votre écran d'accueil.", + "smartMemories": "Souvenirs intelligents", + "pastYearsMemories": "Souvenirs de ces dernières années", + "deleteMultipleAlbumDialog": "Supprimer également les photos (et les vidéos) présentes dans ces {count} albums de tous les autres albums dont ils font partie ?", + "addParticipants": "Ajouter des participants", + "selectedAlbums": "{count} sélectionné(s)", + "actionNotSupportedOnFavouritesAlbum": "Action non prise en charge sur l'album des Favoris", + "onThisDayMemories": "Souvenirs du jour", + "onThisDay": "Ce jour-ci", + "lookBackOnYourMemories": "Regarde tes souvenirs passés 🌄", + "newPhotosEmoji": " nouveau 📸", + "sorryWeHadToPauseYourBackups": "Désolé, nous avons dû mettre en pause vos sauvegardes", + "clickToInstallOurBestVersionYet": "Cliquez pour installer notre meilleure version", + "onThisDayNotificationExplanation": "Recevoir des rappels sur les souvenirs de cette journée des années précédentes.", + "addMemoriesWidgetPrompt": "Ajoutez un gadget des souvenirs à votre écran d'accueil et revenez ici pour le personnaliser.", + "addAlbumWidgetPrompt": "Ajoutez un gadget d'album à votre écran d'accueil et revenez ici pour le personnaliser.", + "addPeopleWidgetPrompt": "Ajoutez un gadget des personnes à votre écran d'accueil et revenez ici pour le personnaliser." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_id.arb b/mobile/lib/l10n/intl_id.arb index a83968f188..a3a4e66d10 100644 --- a/mobile/lib/l10n/intl_id.arb +++ b/mobile/lib/l10n/intl_id.arb @@ -1,7 +1,10 @@ { "@@locale ": "en", "enterYourEmailAddress": "Masukkan alamat email kamu", + "enterYourNewEmailAddress": "Masukkan alamat email baru anda", "accountWelcomeBack": "Selamat datang kembali!", + "emailAlreadyRegistered": "Email sudah terdaftar.", + "emailNotRegistered": "Email belum terdaftar.", "email": "Email", "cancel": "Batal", "verify": "Verifikasi", @@ -185,6 +188,7 @@ }, "allowAddPhotosDescription": "Izinkan orang yang memiliki link untuk menambahkan foto ke album berbagi ini.", "passwordLock": "Kunci dengan sandi", + "canNotOpenTitle": "Tidak dapat membuka album ini", "disableDownloadWarningTitle": "Perlu diketahui", "disableDownloadWarningBody": "Orang yang melihat masih bisa mengambil tangkapan layar atau menyalin foto kamu menggunakan alat eksternal", "allowDownloads": "Izinkan pengunduhan", @@ -327,6 +331,7 @@ "removingFromFavorites": "Menghapus dari favorit...", "sorryCouldNotAddToFavorites": "Maaf, tidak dapat menambahkan ke favorit!", "sorryCouldNotRemoveFromFavorites": "Maaf, tidak dapat menghapus dari favorit!", + "subscribeToEnableSharing": "Anda memerlukan langganan berbayar yang aktif untuk bisa berbagi.", "subscribe": "Berlangganan", "canOnlyRemoveFilesOwnedByYou": "Hanya dapat menghapus berkas yang dimiliki oleh mu", "deleteSharedAlbum": "Hapus album bersama?", @@ -396,6 +401,8 @@ "description": "The text to display in the advanced settings section" }, "photoGridSize": "Ukuran kotak foto", + "manageDeviceStorage": "Mengelola cache perangkat", + "manageDeviceStorageDesc": "Tinjau dan hapus penyimpanan cache lokal.", "machineLearning": "Pemelajaran mesin", "mlConsent": "Aktifkan pemelajaran mesin", "mlConsentTitle": "Aktifkan pemelajaran mesin?", @@ -403,8 +410,13 @@ "mlConsentPrivacy": "Klik di sini untuk detail lebih lanjut tentang fitur ini pada kebijakan privasi kami", "mlConsentConfirmation": "Saya memahami, dan bersedia mengaktifkan pemelajaran mesin", "magicSearch": "Penelusuran ajaib", + "discover": "Temukan", + "@discover": { + "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." + }, "discover_identity": "Identitas", "discover_screenshots": "Tangkapan layar", + "discover_receipts": "Tanda Terima", "discover_notes": "Catatan", "discover_memes": "Meme", "discover_babies": "Bayi", @@ -412,6 +424,7 @@ "discover_selfies": "Swafoto", "discover_wallpapers": "Gambar latar", "discover_food": "Makanan", + "discover_celebrations": "Perayaan", "discover_sunset": "Senja", "discover_hills": "Bukit", "mlIndexingDescription": "Perlu diperhatikan bahwa pemelajaran mesin dapat meningkatkan penggunaan data dan baterai perangkat hingga seluruh item selesai terindeks. Gunakan aplikasi desktop untuk pengindeksan lebih cepat, seluruh hasil akan tersinkronkan secara otomatis.", @@ -646,6 +659,8 @@ "startBackup": "Mulai pencadangan", "noPhotosAreBeingBackedUpRightNow": "Tidak ada foto yang sedang dicadangkan sekarang", "grantFullAccessPrompt": "Harap berikan akses ke semua foto di app Pengaturan", + "allowPermTitle": "Izinkan akses ke foto", + "allowPermBody": "Ijinkan akses ke foto Anda dari Pengaturan agar Ente dapat menampilkan dan mencadangkan pustaka Anda.", "openSettings": "Buka Pengaturan", "selectMorePhotos": "Pilih lebih banyak foto", "existingUser": "Masuk", @@ -671,6 +686,7 @@ "type": "text" }, "backupFailed": "Pencadangan gagal", + "sorryBackupFailedDesc": "Maaf, kami tidak dapat mencadangkan berkas ini sekarang, kami akan mencobanya kembali nanti.", "couldNotBackUpTryLater": "Kami tidak dapat mencadangkan data kamu.\nKami akan coba lagi nanti.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente hanya dapat mengenkripsi dan menyimpan file jika kamu berikan izin", "pleaseGrantPermissions": "Harap berikan izin", @@ -723,8 +739,11 @@ "description": "Display text for an action which triggers a restore of item from trash", "type": "text" }, + "moveToAlbum": "Pindahkan ke album", "unarchive": "Keluarkan dari arsip", + "favorite": "Favorit", "shareLink": "Bagikan link", + "collageLayout": "Tata letak", "addToEnte": "Tambah ke Ente", "addToAlbum": "Tambah ke album", "delete": "Hapus", @@ -888,7 +907,6 @@ "didYouKnow": "Tahukah kamu?", "loadingMessage": "Memuat fotomu...", "loadMessage1": "Kamu bisa membagikan langgananmu dengan keluarga", - "loadMessage2": "Kami telah memelihara lebih dari 30 juta kenangan saat ini", "loadMessage3": "Kami menyimpan 3 salinan dari data kamu, salah satunya di tempat pengungsian bawah tanah", "loadMessage7": "App seluler kami berjalan di latar belakang untuk mengenkripsi dan mencadangkan foto yang kamu potret", "loadMessage8": "web.ente.io menyediakan alat pengunggah yang bagus", diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index 222566995a..e96c115f65 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Inserisci il tuo indirizzo email", + "enterYourNewEmailAddress": "Inserisci il tuo nuovo indirizzo email", "accountWelcomeBack": "Bentornato!", "emailAlreadyRegistered": "Email già registrata.", "emailNotRegistered": "Email non registrata.", @@ -371,6 +372,21 @@ "deleteFromBoth": "Elimina da entrambi", "newAlbum": "Nuovo album", "albums": "Album", + "memoryCount": "{count, plural, =0{nessun ricordo} one{{formattedCount} ricordo} other{{formattedCount} ricordi}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} selezionati", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -510,6 +526,7 @@ "viewLargeFiles": "File di grandi dimensioni", "viewLargeFilesDesc": "Visualizza i file che stanno occupando la maggior parte dello spazio di archiviazione.", "noDuplicates": "✨ Nessun doppione", + "youveNoDuplicateFilesThatCanBeCleared": "Non ci sono file duplicati che possono essere eliminati", "success": "Operazione riuscita", "rateUs": "Lascia una recensione", "remindToEmptyDeviceTrash": "Vuota anche \"Cancellati di recente\" da \"Impostazioni\" -> \"Storage\" per avere più spazio libero", @@ -705,6 +722,7 @@ "type": "text" }, "backupFailed": "Backup fallito", + "sorryBackupFailedDesc": "Purtroppo non è stato possibile eseguire il backup del file in questo momento, riproveremo più tardi.", "couldNotBackUpTryLater": "Impossibile eseguire il backup dei tuoi dati.\nRiproveremo più tardi.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente può criptare e conservare i file solo se gliene concedi l'accesso", "pleaseGrantPermissions": "Concedi i permessi", @@ -776,6 +794,14 @@ "share": "Condividi", "unhideToAlbum": "Non nascondere l'album", "restoreToAlbum": "Ripristina l'album", + "moveItem": "{count, plural, =1 {Sposta elemento} other {Sposta elementi}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Aggiungi elemento} other {Aggiungi elementi}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Crea o seleziona album", "selectAlbum": "Seleziona album", "searchByAlbumNameHint": "Nome album", @@ -873,6 +899,7 @@ "authToViewYourMemories": "Autenticati per visualizzare le tue foto", "unlock": "Sblocca", "freeUpSpace": "Libera spazio", + "freeUpSpaceSaving": "{count, plural, =1 {Può essere cancellato per liberare {formattedSize}} other {Possono essere cancellati per liberare {formattedSize}}}", "filesBackedUpInAlbum": "{count, plural, one {1 file} other {{formattedNumber} file}} di quest'album sono stati salvati in modo sicuro", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -903,6 +930,9 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, "freeUpAmount": "Libera {sizeInMBorGB}", "thisEmailIsAlreadyInUse": "Questo indirizzo email è già registrato", "incorrectCode": "Codice sbagliato", @@ -992,7 +1022,7 @@ "didYouKnow": "Lo sapevi che?", "loadingMessage": "Caricando le tue foto...", "loadMessage1": "Puoi condividere il tuo abbonamento con la tua famiglia", - "loadMessage2": "Fino ad oggi abbiamo conservato oltre 30 milioni di ricordi", + "loadMessage2": "Finora abbiamo conservato oltre 200 milioni di ricordi", "loadMessage3": "Teniamo 3 copie dei tuoi dati, uno in un rifugio sotterraneo antiatomico", "loadMessage4": "Tutte le nostre app sono open source", "loadMessage5": "Il nostro codice sorgente e la crittografia hanno ricevuto audit esterni", @@ -1230,6 +1260,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Trova rapidamente le persone per nome", + "addViewers": "{count, plural, =0 {Aggiungi visualizzatore} =1 {Add viewer} other {Aggiungi visualizzatori}}", + "addCollaborators": "{count, plural, =0 {Aggiungi collaboratore} =1 {Aggiungi collaboratore} other {Aggiungi collaboratori}}", "longPressAnEmailToVerifyEndToEndEncryption": "Premi a lungo un'email per verificare la crittografia end to end.", "developerSettingsWarning": "Sei sicuro di voler modificare le Impostazioni sviluppatore?", "developerSettings": "Impostazioni sviluppatore", @@ -1241,9 +1273,12 @@ "createCollaborativeLink": "Crea link collaborativo", "search": "Cerca", "enterPersonName": "Inserisci il nome della persona", + "editEmailAlreadyLinked": "Questa email è già collegata a {name}.", + "viewPersonToUnlink": "Visualizza {name} per scollegare", "enterName": "Aggiungi nome", "savePerson": "Salva persona", "editPerson": "Modifica persona", + "mergedPhotos": "Fotografie unite", "orMergeWithExistingPerson": "O unisci con esistente", "enterDateOfBirth": "Compleanno (Opzionale)", "birthday": "Compleanno", @@ -1350,6 +1385,7 @@ "extraPhotosFound": "Trovate foto aggiuntive", "configuration": "Configurazione", "localIndexing": "Indicizzazione locale", + "processed": "Processato", "resetPerson": "Rimuovi", "areYouSureYouWantToResetThisPerson": "Sei sicuro di voler resettare questa persona?", "allPersonGroupingWillReset": "Tutti i raggruppamenti per questa persona saranno resettati e perderai tutti i suggerimenti fatti per questa persona", @@ -1359,6 +1395,16 @@ "enableMachineLearningBanner": "Abilita l'apprendimento automatico per la ricerca magica e il riconoscimento facciale", "searchDiscoverEmptySection": "Le immagini saranno mostrate qui una volta che l'elaborazione e la sincronizzazione saranno completate", "searchPersonsEmptySection": "Le persone saranno mostrate qui una volta che l'elaborazione e la sincronizzazione saranno completate", + "viewersSuccessfullyAdded": "{count, plural, =0 {Added 0 visualizzatori} =1 {Added 1 visualizzatore} other {Added {count} visualizzatori}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Aggiunti 0 collaboratori} =1 {Aggiunto 1 collaboratore} other {Aggiunti {count} collaboratori}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1389,6 +1435,15 @@ } } }, + "typeOfGallerGallerytypeIsNotSupportedForRename": "Il tipo di galleria {galleryType} non è supportato per la rinomina", + "@typeOfGallerGallerytypeIsNotSupportedForRename": { + "placeholders": { + "galleryType": { + "type": "String", + "example": "no network" + } + } + }, "tapToUploadIsIgnoredDue": "Tocca per caricare, il caricamento è attualmente ignorato a causa di {ignoreReason}", "@tapToUploadIsIgnoredDue": { "description": "Shown in upload icon widet, inside a tooltip.", @@ -1425,16 +1480,46 @@ }, "currentlyRunning": "attualmente in esecuzione", "ignored": "ignorato", + "photosCount": "{count, plural, =0 {0 foto} =1 {1 foto} other {{count} foto}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "File", + "searchSectionsLengthMismatch": "Lunghezza sezioni non corrisponde: {snapshotLength} != {searchLength}", + "@searchSectionsLengthMismatch": { + "description": "Appears in search tab page", + "placeholders": { + "snapshotLength": { + "type": "int", + "example": "1" + }, + "searchLength": { + "type": "int", + "example": "2" + } + } + }, + "selectMailApp": "Seleziona app email", + "selectAllShort": "Tutte", + "@selectAllShort": { + "description": "Text that appears in bottom right when you start to select multiple photos. When clicked, it selects all photos." + }, "selectCoverPhoto": "Seleziona foto di copertina", "newLocation": "Nuova posizione", "faceNotClusteredYet": "Faccia non ancora raggruppata, per favore torna più tardi", + "theLinkYouAreTryingToAccessHasExpired": "Il link a cui stai cercando di accedere è scaduto.", "openFile": "Apri file", "backupFile": "File di backup", "openAlbumInBrowser": "Apri album nel browser", "openAlbumInBrowserTitle": "Utilizza l'app web per aggiungere foto a questo album", "allow": "Consenti", "allowAppToOpenSharedAlbumLinks": "Consenti all'app di aprire link all'album condiviso", + "seePublicAlbumLinksInApp": "Vedi link album pubblici nell'app", "emergencyContacts": "Contatti di emergenza", "acceptTrustInvite": "Accetta l'invito", "declineTrustInvite": "Rifiuta l'invito", @@ -1498,6 +1583,23 @@ "useDifferentPlayerInfo": "Hai problemi a riprodurre questo video? Premi a lungo qui per provare un altro lettore.", "hideSharedItemsFromHomeGallery": "Nascondi gli elementi condivisi dalla galleria principale", "gallery": "Galleria", + "joinAlbum": "Unisciti all'album", + "joinAlbumSubtext": "per visualizzare e aggiungere le tue foto", + "joinAlbumSubtextViewer": "per aggiungerla agli album condivisi", + "join": "Unisciti", + "linkEmail": "Link Email", + "link": "Link", + "noEnteAccountExclamation": "Nessun account Ente!", + "orPickFromYourContacts": "o scegli tra i tuoi contatti", + "emailDoesNotHaveEnteAccount": "{email} non ha un account Ente.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, "accountOwnerPersonAppbarTitle": "{title} (Io)", "@accountOwnerPersonAppbarTitle": { "description": "Title of appbar for account owner person", @@ -1522,6 +1624,19 @@ } } }, + "linkPersonToEmailConfirmation": "Questo collegherà {personName} a {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Seleziona il tuo volto", "reassigningLoading": "Riassegnando...", "reassignedToName": "Riassegnato a {name}", "@reassignedToName": { @@ -1531,6 +1646,7 @@ } } }, + "saveChangesBeforeLeavingQuestion": "Salvare le modifiche prima di uscire?", "dontSave": "Non salvare", "thisIsMeExclamation": "Questo sono io!", "linkPerson": "Collega persona", @@ -1538,5 +1654,92 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "processingVideos": "Elaborando video" + "videoStreaming": "Video in streaming", + "processingVideos": "Elaborando video", + "streamDetails": "Dettagli dello streaming", + "processing": "In elaborazione", + "queued": "In coda", + "ineligible": "Non idoneo", + "failed": "Non riuscito", + "playStream": "Riproduci lo streaming", + "playOriginal": "Riproduci originale", + "joinAlbumConfirmationDialogBody": "Unirsi a un album renderà visibile la tua email ai suoi partecipanti.", + "pleaseWaitThisWillTakeAWhile": "Attendere, potrebbe volerci un po' di tempo.", + "editTime": "Modifica orario", + "selectTime": "Imposta ora", + "selectDate": "Imposta data", + "previous": "Precedente", + "selectOneDateAndTimeForAll": "Seleziona una data e un'ora per tutti", + "selectStartOfRange": "Seleziona inizio dell'intervallo", + "thisWillMakeTheDateAndTimeOfAllSelected": "In questo modo la data e l'ora di tutte le foto selezionate saranno uguali.", + "allWillShiftRangeBasedOnFirst": "Questo è il primo nel gruppo. Altre foto selezionate si sposteranno automaticamente in base a questa nuova data", + "newRange": "Nuovo intervallo", + "selectOneDateAndTime": "Seleziona data e orario", + "moveSelectedPhotosToOneDate": "Sposta foto selezionate in una data specifica", + "shiftDatesAndTime": "Sposta date e orari", + "photosKeepRelativeTimeDifference": "Le foto mantengono una differenza di tempo relativa", + "photocountPhotos": "{count, plural, =0 {Nessuna foto} =1 {1 foto} other {{count} foto}}", + "@photocountPhotos": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, + "appIcon": "Icona dell'app", + "notThisPerson": "Non è questa persona?", + "selectedItemsWillBeRemovedFromThisPerson": "Gli elementi selezionati verranno rimossi da questa persona, ma non eliminati dalla tua libreria.", + "throughTheYears": "{dateFormat} negli anni", + "thisWeekThroughTheYears": "Questa settimana negli anni", + "thisWeekXYearsAgo": "{count, plural, =1 {Questa settimana, {count} anno fa} other {Questa settimana, {count} anni fa}}", + "youAndThem": "Tu e {name}", + "embracingThem": "Abbracciando {name}", + "partyWithThem": "Festa con {name}", + "hikingWithThem": "Escursioni con {name}", + "feastingWithThem": "Festeggiando con {name}", + "selfiesWithThem": "Selfie con {name}", + "posingWithThem": "In posa con {name}", + "backgroundWithThem": "Bellissimi panorami con {name}", + "sportsWithThem": "Sport con {name}", + "roadtripWithThem": "Viaggio con {name}", + "spotlightOnYourself": "Tu in primo piano", + "spotlightOnThem": "Riflettori su {name}", + "personIsAge": "{name} ha {age}!", + "personTurningAge": "{name} sta per compiere {age} anni", + "lastTimeWithThem": "Ultima volta con {name}", + "tripToLocation": "Viaggio a {location}", + "tripInYear": "Viaggio nel {year}", + "lastYearsTrip": "Viaggio dello scorso anno", + "sunrise": "All'orizzonte", + "mountains": "Oltre le colline", + "greenery": "In mezzo al verde", + "beach": "Sabbia e mare", + "city": "In città", + "moon": "Al chiaro di luna", + "onTheRoad": "Un altro viaggio su strada", + "food": "Delizia culinaria", + "pets": "Compagni pelosetti", + "curatedMemories": "Ricordi importanti", + "widgets": "Widget", + "memories": "Ricordi", + "peopleWidgetDesc": "Seleziona le persone che desideri vedere nella schermata principale.", + "albumsWidgetDesc": "Seleziona gli album che desideri vedere nella schermata principale.", + "memoriesWidgetDesc": "Seleziona il tipo di ricordi che desideri vedere nella schermata principale.", + "smartMemories": "Ricordi intelligenti", + "pastYearsMemories": "Ricordi degli ultimi anni", + "deleteMultipleAlbumDialog": "Eliminare anche le foto (e i video) presenti su {count} album da tutti gli altri album di cui fanno parte?", + "addParticipants": "Aggiungi Partecipanti", + "selectedAlbums": "{count} selezionati", + "actionNotSupportedOnFavouritesAlbum": "Questa azione non è supportata nei Preferiti", + "onThisDayMemories": "Ricordi di questo giorno", + "onThisDay": "In questo giorno", + "lookBackOnYourMemories": "Rivivi i tuoi ricordi 🌄", + "newPhotosEmoji": " nuova 📸", + "sorryWeHadToPauseYourBackups": "Spiacenti, abbiamo dovuto mettere in pausa i backup", + "clickToInstallOurBestVersionYet": "Clicca per installare l'ultima versione dell'app", + "onThisDayNotificationExplanation": "Ricevi promemoria sui ricordi da questo giorno negli anni precedenti.", + "addMemoriesWidgetPrompt": "Aggiungi un widget dei ricordi nella schermata iniziale e torna qui per personalizzarlo.", + "addAlbumWidgetPrompt": "Aggiungi un widget per gli album nella schermata iniziale e torna qui per personalizzarlo.", + "addPeopleWidgetPrompt": "Aggiungi un widget delle persone nella schermata iniziale e torna qui per personalizzarlo." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ja.arb b/mobile/lib/l10n/intl_ja.arb index 7b5bca172c..63a175ed0f 100644 --- a/mobile/lib/l10n/intl_ja.arb +++ b/mobile/lib/l10n/intl_ja.arb @@ -993,7 +993,6 @@ "didYouKnow": "ご存知ですか?", "loadingMessage": "あなたの写真を読み込み中...", "loadMessage1": "サブスクリプションを家族と共有できます", - "loadMessage2": "私たちはこれまでに3000万以上の思い出を保存してきました", "loadMessage3": "私たちはあなたのデータのコピーを3つ保管しています。1つは地下のシェルターにあります。", "loadMessage4": "すべてのアプリはオープンソースです", "loadMessage5": "当社のソースコードと暗号方式は外部から監査されています", @@ -1602,7 +1601,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "動画ストリーミング", "processingVideos": "動画を処理中", "streamDetails": "動画の詳細", "processing": "処理中", @@ -1668,15 +1666,5 @@ "moon": "月明かりの中", "onTheRoad": "再び道で", "food": "料理を楽しむ", - "pets": "毛むくじゃらな仲間たち", - "cLIcon": "新しいアイコン", - "cLIconDesc": "新しいアプリのアイコンが登場です!ちなみに、古いアイコンが好きだった人のためにアイコンの切り替え機能も用意がございます。", - "cLMemories": "思い出", - "cLMemoriesDesc": "あなたにとって特別な瞬間を再発見しましょう。 - あなたの好きな人々、あなたの旅行や休日。 機械学習をオンにすると、自分自身にタグを付けたり、友達の顔に名前をつけたりすることができます。", - "cLWidgets": "ウィジェット", - "cLWidgetsDesc": "Enteの「思い出」機能と統合されたホーム画面ウィジェットが利用可能になりました!", - "cLFamilyPlan": "ファミリープランの制限", - "cLFamilyPlanDesc": "ファミリーメンバーが使用できるストレージ容量の制限を設定できるようになりました。", - "cLBulkEdit": "日付を一括編集", - "cLBulkEditDesc": "複数の写真を、1回の操作で日付/時刻を編集することもできるようになりました。" + "pets": "毛むくじゃらな仲間たち" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_lt.arb b/mobile/lib/l10n/intl_lt.arb index cd1f6203cd..9551c96913 100644 --- a/mobile/lib/l10n/intl_lt.arb +++ b/mobile/lib/l10n/intl_lt.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Įveskite savo el. pašto adresą", + "enterYourNewEmailAddress": "Įveskite savo naują el. pašto adresą", "accountWelcomeBack": "Sveiki sugrįžę!", "emailAlreadyRegistered": "El. paštas jau užregistruotas.", "emailNotRegistered": "El. paštas neregistruotas.", @@ -156,7 +157,7 @@ "confirmYourRecoveryKey": "Patvirtinkite savo atkūrimo raktą", "addViewer": "Pridėti žiūrėtoją", "addCollaborator": "Pridėti bendradarbį", - "addANewEmail": "Pridėti naują el. paštą", + "addANewEmail": "Įtraukite naują el. paštą", "orPickAnExistingOne": "Arba pasirinkite esamą", "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Bendradarbiai gali pridėti nuotraukų ir vaizdo įrašų į bendrintą albumą.", "enterEmail": "Įveskite el. paštą", @@ -371,7 +372,7 @@ "deleteFromBoth": "Ištrinti iš abiejų", "newAlbum": "Naujas albumas", "albums": "Albumai", - "memoryCount": "{count, plural, =0 {nėra prisiminimų} one {{formattedCount} prisiminimas} few {{formattedCount} prisiminimai} many {{formattedCount} prisiminimo} other {{formattedCount} prisiminimų}}", + "memoryCount": "{count, plural, =0 {nėra prisiminimų} one {{formattedCount} prisiminimas} other {{formattedCount} prisiminimų}}", "@memoryCount": { "description": "The text to display the number of memories", "type": "text", @@ -458,8 +459,8 @@ "selectAll": "Pasirinkti viską", "skip": "Praleisti", "updatingFolderSelection": "Atnaujinamas aplankų pasirinkimas...", - "itemCount": "{count, plural, one{{count} elementas} few {{count} elementai} many {{count} elemento} other{{count} elementų}}", - "deleteItemCount": "{count, plural, =1 {Ištrinti {count} elementą} few {Ištrinti {count} elementus} many {Ištrinti {count} elemento} other {Ištrinti {count} elementų}}", + "itemCount": "{count, plural, one{{count} elementas} other{{count} elementų}}", + "deleteItemCount": "{count, plural, =1 {Ištrinti {count} elementą} other {Ištrinti {count} elementų}}", "duplicateItemsGroup": "{count} failai (-ų), kiekvienas {formattedSize}", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -476,7 +477,7 @@ } }, "showMemories": "Rodyti prisiminimus", - "yearsAgo": "{count, plural, one{prieš {count} metus} few {prieš {count} metus} many {prieš {count} metų} other{prieš {count} metų}}", + "yearsAgo": "{count, plural, one{prieš {count} metus} other{prieš {count} metų}}", "backupSettings": "Atsarginės kopijos nustatymai", "backupStatus": "Atsarginės kopijos būsena", "backupStatusDescription": "Čia bus rodomi elementai, kurių atsarginės kopijos buvo sukurtos.", @@ -542,7 +543,7 @@ }, "remindToEmptyEnteTrash": "Taip pat ištuštinkite šiukšlinę, kad gautumėte laisvos vietos.", "sparkleSuccess": "✨ Sėkmė", - "duplicateFileCountWithStorageSaved": "Išvalėte {count, plural, one{{count} dubliuojantį failą} few {{count} dubliuojančius failus} many {{count} dubliuojančio failo} other{{count} dubliuojančių failų}}, išsaugodami ({storageSaved}).", + "duplicateFileCountWithStorageSaved": "Išvalėte {count, plural, one{{count} dubliuojantį failą} other{{count} dubliuojančių failų}}, išsaugodami ({storageSaved})", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -561,7 +562,7 @@ "referrals": "Rekomendacijos", "notifications": "Pranešimai", "sharedPhotoNotifications": "Naujos bendrintos nuotraukos", - "sharedPhotoNotificationsExplanation": "Gaukite pranešimus, kai kas nors prideda nuotrauką į bendrinamą albumą, kuriame dalyvaujate.", + "sharedPhotoNotificationsExplanation": "Gaukite pranešimus, kai kas nors įtraukia nuotrauką į bendrinamą albumą, kuriame dalyvaujate.", "advanced": "Išplėstiniai", "general": "Bendrieji", "security": "Saugumas", @@ -693,8 +694,11 @@ "startBackup": "Pradėti kurti atsarginę kopiją", "noPhotosAreBeingBackedUpRightNow": "Šiuo metu nekuriamos atsarginės nuotraukų kopijos", "preserveMore": "Išsaugoti daugiau", + "grantFullAccessPrompt": "Leiskite prieigą prie visų nuotraukų nustatymų programoje.", "allowPermTitle": "Leisti prieigą prie nuotraukų", "allowPermBody": "Iš nustatymų leiskite prieigą prie nuotraukų, kad „Ente“ galėtų rodyti ir kurti atsargines bibliotekos kopijas.", + "openSettings": "Atverti nustatymus", + "selectMorePhotos": "Pasirinkti daugiau nuotraukų", "existingUser": "Esamas naudotojas", "privateBackups": "Privačios atsarginės kopijos", "forYourMemories": "jūsų prisiminimams", @@ -717,7 +721,10 @@ "description": "Button text for raising a support tickets in case of unhandled errors during backup", "type": "text" }, + "backupFailed": "Atsarginė kopija nepavyko", + "sorryBackupFailedDesc": "Atsiprašome, šiuo metu negalėjome sukurti atsarginės šio failo kopijos. Bandysime pakartoti vėliau.", "couldNotBackUpTryLater": "Nepavyko sukurti atsarginės duomenų kopijos.\nBandysime pakartotinai vėliau.", + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "„Ente“ gali užšifruoti ir išsaugoti failus tik tada, jei suteikiate prieigą prie jų.", "pleaseGrantPermissions": "Suteikite leidimus.", "grantPermission": "Suteikti leidimą", "privateSharing": "Privatus bendrinimas", @@ -761,6 +768,10 @@ } }, "permanentlyDelete": "Ištrinti negrįžtamai", + "canOnlyCreateLinkForFilesOwnedByYou": "Galima sukurti nuorodą tik jums priklausantiems failams", + "publicLinkCreated": "Vieša nuoroda sukurta", + "youCanManageYourLinksInTheShareTab": "Nuorodas galite valdyti bendrinimo kortelėje.", + "linkCopiedToClipboard": "Nuoroda nukopijuota į iškarpinę", "restore": "Atkurti", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", @@ -774,26 +785,43 @@ "shareLink": "Bendrinti nuorodą", "createCollage": "Kurti koliažą", "saveCollage": "Išsaugoti koliažą", + "collageSaved": "Koliažas išsaugotas į galeriją", + "collageLayout": "Išdėstymas", "addToEnte": "Pridėti į „Ente“", "addToAlbum": "Pridėti į albumą", "delete": "Ištrinti", "hide": "Slėpti", "share": "Bendrinti", + "unhideToAlbum": "Rodyti į albumą", "restoreToAlbum": "Atkurti į albumą", - "moveItem": "{count, plural, one {Perkelti elementą} few {Perkelti elementus} many {Perkelti elemento} =1 {Perkelti elementą} other {Perkelti elementų}}", + "moveItem": "{count, plural, =1 {Perkelti elementą} other {Perkelti elementų}}", "@moveItem": { "description": "Page title while moving one or more items to an album" }, - "addItem": "{count, plural, one {Pridėti elementą} few {Pridėti elementus} many {Pridėti elemento} =1 {Pridėti elementą} other {Pridėti elementų}}", + "addItem": "{count, plural, =1 {Pridėti elementą} other {Pridėti elementų}}", "@addItem": { "description": "Page title while adding one or more items to album" }, "createOrSelectAlbum": "Kurkite arba pasirinkite albumą", + "selectAlbum": "Pasirinkti albumą", "searchByAlbumNameHint": "Albumo pavadinimas", "albumTitle": "Albumo pavadinimas", "enterAlbumName": "Įveskite albumo pavadinimą", "restoringFiles": "Atkuriami failai...", + "movingFilesToAlbum": "Perkeliami failai į albumą...", + "unhidingFilesToAlbum": "Rodomi failai į albumą", + "canNotUploadToAlbumsOwnedByOthers": "Negalima įkelti į kitiems priklausančius albumus", + "uploadingFilesToAlbum": "Įkeliami failai į albumą...", + "addedSuccessfullyTo": "Sėkmingai įtraukta į „{albumName}“", + "movedSuccessfullyTo": "Sėkmingai perkelta į „{albumName}“", + "thisAlbumAlreadyHDACollaborativeLink": "Šis albumas jau turi bendradarbiavimo nuorodą.", + "collaborativeLinkCreatedFor": "Bendradarbiavimo nuoroda sukurta albumui „{albumName}“", + "askYourLovedOnesToShare": "Paprašykite savo artimuosius bendrinti", + "invite": "Kviesti", + "shareYourFirstAlbum": "Bendrinkite savo pirmąjį albumą", + "sharedWith": "Bendrinta su {emailIDs}", "sharedWithMe": "Bendrinta su manimi", + "sharedByMe": "Bendrinta manimi", "doubleYourStorage": "Padvigubinkite saugyklą", "referFriendsAnd2xYourPlan": "Rekomenduokite draugams ir 2 kartus padidinkite savo planą", "shareAlbumHint": "Atidarykite albumą ir palieskite bendrinimo mygtuką viršuje dešinėje, kad bendrintumėte.", @@ -809,6 +837,8 @@ } }, "deleteAll": "Ištrinti viską", + "renameAlbum": "Pervadinti albumą", + "convertToAlbum": "Konvertuoti į albumą", "setCover": "Nustatyti viršelį", "@setCover": { "description": "Text to set cover photo for an album" @@ -820,15 +850,27 @@ "leaveSharedAlbum": "Palikti bendrinamą albumą?", "leaveAlbum": "Palikti albumą", "photosAddedByYouWillBeRemovedFromTheAlbum": "Jūsų pridėtos nuotraukos bus pašalintos iš albumo", + "youveNoFilesInThisAlbumThatCanBeDeleted": "Neturite šiame albume failų, kuriuos būtų galima ištrinti.", "youDontHaveAnyArchivedItems": "Neturite jokių archyvuotų elementų.", + "ignoredFolderUploadReason": "Kai kurie šio albumo failai ignoruojami, nes anksčiau buvo ištrinti iš „Ente“.", + "resetIgnoredFiles": "Atkurti ignoruojamus failus", + "deviceFilesAutoUploading": "Į šį įrenginio albumą įtraukti failai bus automatiškai įkelti į „Ente“.", + "turnOnBackupForAutoUpload": "Įjunkite atsarginės kopijos kūrimą, kad automatiškai įkeltumėte į šį įrenginio aplanką įtrauktus failus į „Ente“.", + "noHiddenPhotosOrVideos": "Nėra paslėptų nuotraukų arba vaizdo įrašų", "toHideAPhotoOrVideo": "Kad paslėptumėte nuotrauką ar vaizdo įrašą", "openTheItem": "• Atverkite elementą.", "clickOnTheOverflowMenu": "• Spustelėkite ant perpildymo meniu", + "click": "• Spauskite", "nothingToSeeHere": "Čia nėra nieko, ką pamatyti. 👀", "unarchiveAlbum": "Išarchyvuoti albumą", "archiveAlbum": "Archyvuoti albumą", + "calculating": "Skaičiuojama...", + "pleaseWaitDeletingAlbum": "Palaukite. Ištrinamas albumas", "searchByExamples": "• Albumų pavadinimai (pvz., „Fotoaparatas“)\n• Failų tipai (pvz., „Vaizdo įrašai“, „.gif“)\n• Metai ir mėnesiai (pvz., „2022“, „sausis“)\n• Šventės (pvz., „Kalėdos“)\n• Nuotraukų aprašymai (pvz., „#džiaugsmas“)", + "youCanTrySearchingForADifferentQuery": "Galite pabandyti ieškoti pagal kitą užklausą.", "noResultsFound": "Rezultatų nerasta.", + "addedBy": "Įtraukė {emailOrName}", + "loadingExifData": "Įkeliami EXIF duomenys...", "viewAllExifData": "Peržiūrėti visus EXIF duomenis", "noExifData": "Nėra EXIF duomenų", "thisImageHasNoExifData": "Šis vaizdas neturi Exif duomenų", @@ -848,9 +890,50 @@ "deduplicateFiles": "Atdubliuoti failus", "deselectAll": "Naikinti visų pasirinkimą", "reviewDeduplicateItems": "Peržiūrėkite ir ištrinkite elementus, kurie, jūsų manymu, yra dublikatai.", + "clubByCaptureTime": "Grupuoti pagal užfiksavimo laiką", + "clubByFileName": "Grupuoti pagal failo pavadinimą", + "count": "Skaičių", + "totalSize": "Bendrą dydį", + "longpressOnAnItemToViewInFullscreen": "Ilgai paspauskite elementą, kad peržiūrėtumėte per visą ekraną", + "decryptingVideo": "Iššifruojamas vaizdo įrašas...", + "authToViewYourMemories": "Nustatykite tapatybę, kad peržiūrėtumėte savo prisiminimus", "unlock": "Atrakinti", "freeUpSpace": "Atlaisvinti vietos", - "freeUpAccessPostDelete": "Vis dar galite pasiekti {count, plural, one {jį} few {juos} many {juos} =1 {jį} other {jų}} platformoje „Ente“, kol turite aktyvų prenumeratą.", + "freeUpSpaceSaving": "{count, plural, =1 {Jį galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}} other {Jų galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}}}", + "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija} few {{formattedNumber} failai šiame albume saugiai sukurtos atsarginės kopijos} many {{formattedNumber} failo šiame albume saugiai sukurtos atsargines kopijos} other {{formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija}}.", + "@filesBackedUpInAlbum": { + "description": "Text to tell user how many files have been backed up in the album", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedNumber": { + "content": "{formattedNumber}", + "example": "1,000", + "type": "String" + } + } + }, + "filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} failas šiame įrenginyje saugiai sukurta atsarginė kopija} few {{formattedNumber} failai šiame įrenginyje saugiai sukurtos atsarginės kopijos} many {{formattedNumber} failo šiame įrenginyje saugiai sukurtos atsargines kopijos} other {{formattedNumber} failų šiame įrenginyje saugiai sukurta atsarginių kopijų}}.", + "@filesBackedUpFromDevice": { + "description": "Text to tell user how many files have been backed up from this device", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedNumber": { + "content": "{formattedNumber}", + "example": "1,000", + "type": "String" + } + } + }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Vis dar galite pasiekti {count, plural, =1 {jį} other {jų}} platformoje „Ente“, kol turite aktyvų prenumeratą.", "@freeUpAccessPostDelete": { "placeholders": { "count": { @@ -870,6 +953,9 @@ "theRecoveryKeyYouEnteredIsIncorrect": "Įvestas atkūrimo raktas yra neteisingas.", "twofactorAuthenticationSuccessfullyReset": "Dvigubas tapatybės nustatymas sėkmingai iš naujo nustatytas.", "pleaseVerifyTheCodeYouHaveEntered": "Patvirtinkite įvestą kodą.", + "pleaseContactSupportIfTheProblemPersists": "Jei problema išlieka, susisiekite su pagalbos komanda.", + "twofactorAuthenticationHasBeenDisabled": "Dvigubas tapatybės nustatymas išjungtas.", + "sorryTheCodeYouveEnteredIsIncorrect": "Atsiprašome, įvestas kodas yra neteisingas.", "yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodas nebegaliojantis.", "emailChangedTo": "El. paštas pakeistas į {newEmail}", "verifying": "Patvirtinama...", @@ -880,6 +966,16 @@ "encryptingBackup": "Šifruojama atsarginė kopija...", "syncStopped": "Sinchronizavimas sustabdytas", "syncProgress": "{completed} / {total} išsaugomi prisiminimai", + "uploadingMultipleMemories": "Išsaugomi {count} prisiminimai...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, + "uploadingSingleMemory": "Išsaugomas prisiminimas...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -896,12 +992,20 @@ "successfullyArchived": "Sėkmingai suarchyvuota", "successfullyUnarchived": "Sėkmingai išarchyvuota", "renameFile": "Pervadinti failą", + "enterFileName": "Įveskite failo pavadinimą", + "filesDeleted": "Failai ištrinti", "selectedFilesAreNotOnEnte": "Pasirinkti failai nėra platformoje „Ente“", + "thisActionCannotBeUndone": "Šio veiksmo negalima anuliuoti.", "emptyTrash": "Ištuštinti šiukšlinę?", + "permDeleteWarning": "Visi elementai šiukšlinėje bus negrįžtamai ištrinti.\n\nŠio veiksmo negalima anuliuoti.", "empty": "Ištuštinti", "couldNotFreeUpSpace": "Nepavyko atlaisvinti vietos.", "permanentlyDeleteFromDevice": "Ištrinti negrįžtamai iš įrenginio?", "someOfTheFilesYouAreTryingToDeleteAre": "Kai kurie failai, kuriuos bandote ištrinti, yra pasiekiami tik jūsų įrenginyje ir jų negalima atkurti, jei jie buvo ištrinti.", + "theyWillBeDeletedFromAllAlbums": "Jie bus ištrinti iš visų albumų.", + "someItemsAreInBothEnteAndYourDevice": "Kai kurie elementai yra ir platformoje „Ente“ bei jūsų įrenginyje.", + "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Pasirinkti elementai bus ištrinti iš visų albumų ir perkelti į šiukšlinę.", + "theseItemsWillBeDeletedFromYourDevice": "Šie elementai bus ištrinti iš jūsų įrenginio.", "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Atrodo, kad kažkas nutiko ne taip. Bandykite pakartotinai po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda.", "error": "Klaida", "tempErrorContactSupportIfPersists": "Atrodo, kad kažkas nutiko ne taip. Bandykite dar kartą po kurio laiko. Jei klaida tęsiasi, susisiekite su mūsų palaikymo komanda.", @@ -922,9 +1026,12 @@ "pleaseSendTheLogsTo": "Siųskite žurnalus adresu\n{toEmail}", "copyEmailAddress": "Kopijuoti el. pašto adresą", "exportLogs": "Eksportuoti žurnalus", + "pleaseEmailUsAt": "Siųskite el. laišką mums adresu {toEmail}.", + "dismiss": "Atmesti", "didYouKnow": "Ar žinojote?", + "loadingMessage": "Įkeliamos jūsų nuotraukos...", "loadMessage1": "Galite bendrinti savo prenumeratą su šeima.", - "loadMessage2": "Iki šiol išsaugojome daugiau kaip 30 milijonų prisiminimų.", + "loadMessage2": "Iki šiol išsaugojome daugiau nei 200 milijonų prisiminimų.", "loadMessage3": "Laikome 3 jūsų duomenų kopijas, vieną iš jų – požeminėje priešgaisrinėje slėptuvėje.", "loadMessage4": "Visos mūsų programos yra atvirojo kodo.", "loadMessage5": "Mūsų šaltinio kodas ir kriptografija buvo išoriškai audituoti.", @@ -932,28 +1039,48 @@ "loadMessage7": "Mūsų mobiliosios programos veikia fone, kad užšifruotų ir sukurtų atsarginę kopiją visų naujų nuotraukų, kurias spustelėjate.", "loadMessage8": "web.ente.io turi sklandų įkėlėją", "loadMessage9": "Naudojame „Xchacha20Poly1305“, kad saugiai užšifruotume jūsų duomenis.", + "photoDescriptions": "Nuotraukų aprašai", + "fileTypesAndNames": "Failų tipai ir pavadinimai", "location": "Vietovė", "moments": "Akimirkos", + "searchFaceEmptySection": "Asmenys bus rodomi čia, kai bus užbaigtas indeksavimas.", + "searchDatesEmptySection": "Ieškokite pagal datą, mėnesį arba metus", "searchLocationEmptySection": "Grupės nuotraukos, kurios padarytos tam tikru spinduliu nuo nuotraukos", "searchPeopleEmptySection": "Pakvieskite asmenis ir čia matysite visas jų bendrinamas nuotraukas.", + "searchAlbumsEmptySection": "Albumai", + "searchFileTypesAndNamesEmptySection": "Failų tipai ir pavadinimai", "searchCaptionEmptySection": "Pridėkite aprašymus, pavyzdžiui, „#kelionė“, į nuotraukos informaciją, kad greičiau jas čia rastumėte.", "language": "Kalba", "selectLanguage": "Pasirinkite kalbą", "locationName": "Vietovės pavadinimas", "addLocation": "Pridėti vietovę", + "groupNearbyPhotos": "Grupuoti netoliese nuotraukas", "kiloMeterUnit": "km", "addLocationButton": "Pridėti", + "radius": "Spindulys", "locationTagFeatureDescription": "Vietos žymė grupuoja visas nuotraukas, kurios buvo padarytos tam tikru spinduliu nuo nuotraukos", "galleryMemoryLimitInfo": "Galerijoje rodoma iki 1000 prisiminimų", - "centerPoint": "Vidurio taškas", + "save": "Išsaugoti", + "centerPoint": "Centro taškas", + "pickCenterPoint": "Pasirinkite centro tašką", + "useSelectedPhoto": "Naudoti pasirinktą nuotrauką", "resetToDefault": "Atkurti numatytąsias reikšmes", "@resetToDefault": { "description": "Button text to reset cover photo to default" }, "edit": "Redaguoti", "deleteLocation": "Ištrinti vietovę", + "rotateLeft": "Sukti į kairę", + "flip": "Apversti", + "rotateRight": "Sukti į dešinę", + "saveCopy": "Išsaugoti kopiją", "light": "Šviesi", "color": "Spalva", + "yesDiscardChanges": "Taip, atmesti pakeitimus", + "doYouWantToDiscardTheEditsYouHaveMade": "Ar norite atmesti atliktus pakeitimus?", + "saving": "Išsaugoma...", + "editsSaved": "Redagavimai išsaugoti", + "oopsCouldNotSaveEdits": "Ups, nepavyko išsaugoti redagavimų.", "distanceInKMUnit": "km", "@distanceInKMUnit": { "description": "Unit for distance in km" @@ -962,10 +1089,16 @@ "dayYesterday": "Vakar", "storage": "Saugykla", "usedSpace": "Naudojama vieta", + "storageBreakupFamily": "Šeima", "storageBreakupYou": "Jūs", "@storageBreakupYou": { "description": "Label to indicate how much storage you are using when you are part of a family plan" }, + "storageUsageInfo": "{usedAmount} {usedStorageUnit} iš {totalAmount} {totalStorageUnit} naudojama", + "@storageUsageInfo": { + "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" + }, + "availableStorageSpace": "{freeAmount} {storageUnit} laisva", "appVersion": "Versija: {versionValue}", "verifyIDLabel": "Patvirtinti", "fileInfoAddDescHint": "Pridėti aprašymą...", @@ -974,12 +1107,21 @@ "@setLabel": { "description": "Label of confirm button to add a new custom radius to the radius selector of a location tag" }, + "setRadius": "Nustatyti spindulį", "familyPlanPortalTitle": "Šeima", "familyPlanOverview": "Įtraukite 5 šeimos narius į jūsų esamą planą nemokėdami papildomai.\n\nKiekvienas narys gauna savo asmeninę vietą ir negali matyti vienas kito failų, nebent jie bendrinami.\n\nŠeimos planai pasiekiami klientams, kurie turi mokamą „Ente“ prenumeratą.\n\nPrenumeruokite dabar, kad pradėtumėte!", "androidBiometricHint": "Patvirtinkite tapatybę", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." }, + "androidBiometricNotRecognized": "Neatpažinta. Bandykite dar kartą.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricSuccess": "Sėkmė", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, "androidCancelButton": "Atšaukti", "@androidCancelButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." @@ -1020,6 +1162,8 @@ "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, + "openstreetmapContributors": "„OpenStreetMap“ bendradarbiai", + "hostedAtOsmFrance": "Talpinama OSM Prancūzijoje", "map": "Žemėlapis", "@map": { "description": "Label for the map view" @@ -1027,15 +1171,32 @@ "maps": "Žemėlapiai", "enableMaps": "Įjungti žemėlapius", "enableMapsDesc": "Tai parodys jūsų nuotraukas pasaulio žemėlapyje.\n\nŠį žemėlapį talpina „OpenStreetMap“, o tiksliomis nuotraukų vietovėmis niekada nebendrinama.\n\nŠią funkciją bet kada galite išjungti iš nustatymų.", + "quickLinks": "Sparčios nuorodos", + "selectItemsToAdd": "Pasirinkite elementus įtraukti", + "addSelected": "Pridėti pasirinktus", + "addFromDevice": "Pridėti iš įrenginio", + "addPhotos": "Įtraukti nuotraukų", + "noPhotosFoundHere": "Nuotraukų čia nerasta", + "zoomOutToSeePhotos": "Padidinkite mastelį, kad matytumėte nuotraukas", "noImagesWithLocation": "Nėra vaizdų su vietove", "unpinAlbum": "Atsegti albumą", "pinAlbum": "Prisegti albumą", "create": "Kurti", "viewAll": "Peržiūrėti viską", "nothingSharedWithYouYet": "Kol kas su jumis niekuo nesibendrinama.", + "noAlbumsSharedByYouYet": "Dar nėra albumų, kuriais bendrinotės.", "sharedWithYou": "Bendrinta su jumis", "sharedByYou": "Bendrinta iš jūsų", + "inviteYourFriendsToEnte": "Pakvieskite savo draugus į „Ente“", + "failedToDownloadVideo": "Nepavyko atsisiųsti vaizdo įrašo.", "hiding": "Slepiama...", + "unhiding": "Rodoma...", + "successfullyHid": "Sėkmingai paslėptas", + "successfullyUnhid": "Sėkmingai atslėptas", + "crashReporting": "Pranešti apie strigčius", + "resumableUploads": "Tęstiniai įkėlimai", + "addToHiddenAlbum": "Įtraukti į paslėptą albumą", + "moveToHiddenAlbum": "Perkelti į paslėptą albumą", "fileTypes": "Failų tipai", "deleteConfirmDialogBody": "Ši paskyra susieta su kitomis „Ente“ programomis, jei jas naudojate. Jūsų įkelti duomenys per visas „Ente“ programas bus planuojama ištrinti, o jūsų paskyra bus ištrinta negrįžtamai.", "hearUsWhereTitle": "Kaip išgirdote apie „Ente“? (nebūtina)", @@ -1044,15 +1205,19 @@ "addOns": "Priedai", "addOnPageSubtitle": "Išsami informacija apie priedus", "yourMap": "Jūsų žemėlapis", + "modifyYourQueryOrTrySearchingFor": "Modifikuokite užklausą arba bandykite ieškoti", "blackFridaySale": "Juodojo penktadienio išpardavimas", "upto50OffUntil4thDec": "Iki 50% nuolaida, gruodžio 4 d.", "photos": "Nuotraukos", "videos": "Vaizdo įrašai", "livePhotos": "Gyvos nuotraukos", + "searchHint1": "Sparti paieška įrenginyje", + "searchHint2": "Nuotraukų datos ir aprašai", "searchHint3": "Albumai, failų pavadinimai ir tipai", "searchHint4": "Vietovė", "searchHint5": "Jau netrukus: veidų ir magiškos paieškos ✨", - "searchResultCount": "{count, plural, one{Rastas {count} rezultatas} few {Rasti {count} rezultatai} many {Rasta {count} rezultato} other{Rasta {count} rezultatų}}", + "addYourPhotosNow": "Įtraukite savo nuotraukas dabar", + "searchResultCount": "{count, plural, one{Rastas {count} rezultatas} other{Rasta {count} rezultatų}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1064,6 +1229,7 @@ }, "faces": "Veidai", "people": "Asmenys", + "contents": "Turinys", "addNew": "Pridėti naują", "@addNew": { "description": "Text to add a new item (location tag, album, caption etc)" @@ -1071,7 +1237,9 @@ "contacts": "Kontaktai", "noInternetConnection": "Nėra interneto ryšio", "pleaseCheckYourInternetConnectionAndTryAgain": "Patikrinkite savo interneto ryšį ir bandykite dar kartą.", + "signOutFromOtherDevices": "Atsijungti iš kitų įrenginių", "signOutOtherBody": "Jei manote, kad kas nors gali žinoti jūsų slaptažodį, galite priverstinai atsijungti iš visų kitų įrenginių, naudojančių jūsų paskyrą.", + "signOutOtherDevices": "Atsijungti kitus įrenginius", "doNotSignOut": "Neatsijungti", "editLocation": "Redaguoti vietovę", "selectALocation": "Pasirinkite vietovę", @@ -1101,8 +1269,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Greitai suraskite žmones pagal vardą", - "addViewers": "{count, plural, one {Pridėti žiūrėtoją} few {Pridėti žiūrėtojus} many {Pridėti žiūrėtojo} =0 {Pridėti žiūrėtojų} =1 {Pridėti žiūrėtoją} other {Pridėti žiūrėtojų}}", - "addCollaborators": "{count, plural, one {Pridėti bendradarbį} few {Pridėti bendradarbius} many {Pridėti bendradarbio} =0 {Pridėti bendradarbių} =1 {Pridėti bendradarbį} other {Pridėti bendradarbių}}", + "addViewers": "{count, plural, =0 {Pridėti žiūrėtojų} =1 {Pridėti žiūrėtoją} other {Pridėti žiūrėtojų}}", + "addCollaborators": "{count, plural, =0 {Pridėti bendradarbių} =1 {Pridėti bendradarbį} other {Pridėti bendradarbių}}", "longPressAnEmailToVerifyEndToEndEncryption": "Ilgai paspauskite el. paštą, kad patvirtintumėte visapusį šifravimą.", "developerSettingsWarning": "Ar tikrai norite modifikuoti kūrėjo nustatymus?", "developerSettings": "Kūrėjo nustatymai", @@ -1114,6 +1282,8 @@ "createCollaborativeLink": "Kurti bendradarbiavimo nuorodą", "search": "Ieškokite", "enterPersonName": "Įveskite asmens vardą", + "editEmailAlreadyLinked": "Šis el. paštas jau susietas su {name}.", + "viewPersonToUnlink": "Peržiūrėkite {name}, kad atsietumėte", "enterName": "Įveskite vardą", "savePerson": "Išsaugoti asmenį", "editPerson": "Redaguoti asmenį", @@ -1126,6 +1296,7 @@ "manualPairDesc": "Susieti su PIN kodu veikia bet kuriame ekrane, kuriame norite peržiūrėti albumą.", "connectToDevice": "Prijungti prie įrenginio", "autoCastDialogBody": "Čia matysite pasiekiamus perdavimo įrenginius.", + "autoCastiOSPermission": "Įsitikinkite, kad programai „Ente“ nuotraukos yra įjungti vietinio tinklo leidimai, nustatymuose.", "noDeviceFound": "Įrenginys nerastas", "stopCastingTitle": "Stabdyti perdavimą", "stopCastingBody": "Ar norite sustabdyti perdavimą?", @@ -1146,6 +1317,7 @@ "right": "Dešinė", "whatsNew": "Kas naujo", "reviewSuggestions": "Peržiūrėti pasiūlymus", + "review": "Peržiūrėti", "useAsCover": "Naudoti kaip viršelį", "notPersonLabel": "Ne {name}?", "@notPersonLabel": { @@ -1223,6 +1395,7 @@ "configuration": "Konfiguracija", "localIndexing": "Vietinis indeksavimas", "processed": "Apdorota", + "resetPerson": "Šalinti", "areYouSureYouWantToResetThisPerson": "Ar tikrai norite iš naujo nustatyti šį asmenį?", "allPersonGroupingWillReset": "Visi šio asmens grupavimai bus iš naujo nustatyti, o jūs neteksite visų šiam asmeniui pateiktų pasiūlymų", "yesResetPerson": "Taip, nustatyti asmenį iš naujo", @@ -1231,7 +1404,7 @@ "enableMachineLearningBanner": "Įjunkite mašininį mokymąsi magiškai paieškai ir veidų atpažinimui", "searchDiscoverEmptySection": "Vaizdai bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.", "searchPersonsEmptySection": "Asmenys bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.", - "viewersSuccessfullyAdded": "{count, plural, one {Pridėtas {count} žiūrėtojas} few {Pridėti {count} žiūrėtojai} many {Pridėta {count} žiūrėtojo} =0 {Pridėta 0 žiūrėtojų} =1 {Pridėtas 1 žiūrėtojas} other {Pridėta {count} žiūrėtojų}}", + "viewersSuccessfullyAdded": "{count, plural, =0 {Įtraukta 0 žiūrėtojų} =1 {Įtrauktas 1 žiūrėtojas} other {Įtraukta {count} žiūrėtojų}}", "@viewersSuccessfullyAdded": { "placeholders": { "count": { @@ -1316,7 +1489,7 @@ }, "currentlyRunning": "šiuo metu vykdoma", "ignored": "ignoruota", - "photosCount": "{count, plural, one {{count} nuotrauka} few {{count} nuotraukos} many {{count} nuotraukos} =0 {0 nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", + "photosCount": "{count, plural, =0 {0 nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", "@photosCount": { "placeholders": { "count": { @@ -1401,7 +1574,7 @@ "trustedInviteBody": "Buvote pakviesti tapti {email} palikimo kontaktu.", "warning": "Įspėjimas", "proceed": "Tęsti", - "confirmAddingTrustedContact": "Ketinate pridėti {email} kaip patikimą kontaktą. Jie galės atkurti jūsų paskyrą, jei jūsų nebus {numOfDays} dienų.", + "confirmAddingTrustedContact": "Ketinate įtraukti {email} kaip patikimą kontaktą. Jie galės atkurti jūsų paskyrą, jei jūsų nebus {numOfDays} dienų.", "@confirmAddingTrustedContact": { "placeholders": { "email": { @@ -1445,7 +1618,21 @@ } } }, + "reassignMe": "Perskirstyti „Aš“", "me": "Aš", + "linkEmailToContactBannerCaption": "spartesniam bendrinimui", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Pasirinkite asmenį, kurį susieti.", + "linkPersonToEmail": "Susieti asmenį su {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, "linkPersonToEmailConfirmation": "Tai susies {personName} su {email}.", "@linkPersonToEmailConfirmation": { "description": "Confirmation message when linking a person to an email", @@ -1458,6 +1645,8 @@ } } }, + "selectYourFace": "Pasirinkite savo veidą", + "reassigningLoading": "Perskirstoma...", "reassignedToName": "Perskirstė jus į {name}", "@reassignedToName": { "placeholders": { @@ -1470,11 +1659,11 @@ "dontSave": "Neišsaugoti", "thisIsMeExclamation": "Tai aš!", "linkPerson": "Susiekite asmenį,", - "linkPersonCaption": "kad geriau bendrintumėte patirtį", + "linkPersonCaption": "geresniam bendrinimo patirčiai", "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Vaizdo įrašų srautinis perdavimas", + "videoStreaming": "Srautiniai vaizdo įrašai", "processingVideos": "Apdorojami vaizdo įrašai", "streamDetails": "Srautinio perdavimo išsami informacija", "processing": "Apdorojama", @@ -1498,7 +1687,7 @@ "moveSelectedPhotosToOneDate": "Perkelti pasirinktas nuotraukas į vieną datą", "shiftDatesAndTime": "Pastumti datas ir laiką", "photosKeepRelativeTimeDifference": "Nuotraukos išlaiko santykinį laiko skirtumą", - "photocountPhotos": "{count, plural, one {{count} nuotrauka} few {{count} nuotraukos} many {{count} nuotraukos} =0 {Nėra nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", + "photocountPhotos": "{count, plural, =0 {Nėra nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}", "@photocountPhotos": { "placeholders": { "count": { @@ -1512,10 +1701,23 @@ "selectedItemsWillBeRemovedFromThisPerson": "Pasirinkti elementai bus pašalinti iš šio asmens, bet nebus ištrinti iš jūsų bibliotekos.", "throughTheYears": "{dateFormat} per metus", "thisWeekThroughTheYears": "Ši savaitė per metus", - "thisWeekXYearsAgo": "{count, plural, one {Šią savaitę, prieš {count} metus} few {Šią savaitę, prieš {count} metus} many {Šią savaitę, prieš {count} metų}=1 {Šią savaitę, prieš {count} metus} other {Šią savaitę, prieš {count} metų}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Šią savaitę, prieš {count} metus} other {Šią savaitę, prieš {count} metų}}", "youAndThem": "Jūs ir {name}", "admiringThem": "Žavisi {name}", + "embracingThem": "Apkabinat {name}", + "partyWithThem": "Vakarėlis su {name}", + "hikingWithThem": "Žygiavimas su {name}", + "feastingWithThem": "Vaišiavimas su {name}", + "selfiesWithThem": "Asmenukės su {name}", + "posingWithThem": "Pozavimas su {name}", "backgroundWithThem": "Gražūs vaizdai su {name}", + "sportsWithThem": "Sportai su {name}", + "roadtripWithThem": "Kelionė su {name}", + "spotlightOnYourself": "Dėmesys į save", + "spotlightOnThem": "Dėmesys {name}", + "personIsAge": "{name} yra {age} m.!", + "personTurningAge": "{name} netrukus sulauks {age} m.", + "lastTimeWithThem": "Paskutinį kartą su {name}", "tripToLocation": "Kelionė į {location}", "tripInYear": "Kelionė per {year}", "lastYearsTrip": "Pastarųjų metų kelionė", @@ -1527,15 +1729,27 @@ "moon": "Mėnulio šviesoje", "onTheRoad": "Vėl kelyje", "food": "Kulinarinis malonumas", - "cLIcon": "Nauja piktograma", - "cLIconDesc": "Pagaliau – nauja programos piktograma, kuri, mūsų manymu, geriausiai atspindi mūsų kūrybą. Taip pat pridėjome piktogramos perjungiklį, tad galite ir toliau naudoti senąją piktogramą.", - "cLMemories": "Prisiminimai", - "cLMemoriesDesc": "Iš naujo atraskite ypatingas akimirkas – atkreipkite dėmesį į mėgstamus asmenis, keliones ir atostogas, geriausias nuotraukas bei daug daugiau. Įjunkite mašininį mokymąsi, pažymėkite save ir įvardykite draugus dėl geriausios patirties.", - "cLWidgets": "Valdikliai", - "cLWidgetsDesc": "Dabar galima naudoti su prisiminimais integruotus pagrindinio ekrano valdiklius. Jie parodys jūsų ypatingas akimirkas neatvėrus programos.", - "cLFamilyPlan": "Šeimos plano ribos", - "cLFamilyPlanDesc": "Dabar galite nustatyti ribas, kiek saugyklos gali naudoti jūsų šeimos nariai.", - "cLBulkEdit": "Masiškai redaguokite datas", - "cLBulkEditDesc": "Dabar galite pasirinkti kelias nuotraukas ir vienu sparčiu veiksmu redaguoti visų nuotraukų datą ir laiką. Taip pat palaikomas datų perkėlimas.", - "curatedMemories": "Kuruoti prisiminimai" + "pets": "Furio draugai", + "curatedMemories": "Kuruoti prisiminimai", + "widgets": "Valdikliai", + "memories": "Prisiminimai", + "peopleWidgetDesc": "Pasirinkite asmenis, kuriuos norite matyti savo pradžios ekrane.", + "albumsWidgetDesc": "Pasirinkite albumus, kuriuos norite matyti savo pradžios ekrane.", + "memoriesWidgetDesc": "Pasirinkite, kokius prisiminimus norite matyti savo pradžios ekrane.", + "smartMemories": "Išmanieji prisiminimai", + "pastYearsMemories": "Praėjusių metų prisiminimai", + "deleteMultipleAlbumDialog": "Taip pat ištrinti nuotraukas (ir vaizdo įrašus), esančias šiuose {count} albumuose, iš visų kitų albumų, kuriuose jos yra dalis?", + "addParticipants": "Įtraukti dalyvių", + "selectedAlbums": "{count} pasirinkta", + "actionNotSupportedOnFavouritesAlbum": "Veiksmas nepalaikomas Mėgstamų albume.", + "onThisDayMemories": "Šios dienos prisiminimai", + "onThisDay": "Šią dieną", + "lookBackOnYourMemories": "Pažvelkite atgal į savo prisiminimus 🌄", + "newPhotosEmoji": " naujas 📸", + "sorryWeHadToPauseYourBackups": "Atsiprašome, turėjome pristabdyti jūsų atsarginių kopijų kūrimą.", + "clickToInstallOurBestVersionYet": "Spustelėkite, kad įdiegtumėte geriausią mūsų versiją iki šiol", + "onThisDayNotificationExplanation": "Gaukite priminimus apie praėjusių metų šios dienos prisiminimus.", + "addMemoriesWidgetPrompt": "Pridėkite prisiminimų valdiklį prie savo pradžios ekrano ir grįžkite čia, kad tinkintumėte.", + "addAlbumWidgetPrompt": "Pridėkite albumo valdiklį prie savo pradžios ekrano ir grįžkite čia, kad tinkintumėte.", + "addPeopleWidgetPrompt": "Pridėkite asmenų valdiklį prie savo pradžios ekrano ir grįžkite čia, kad tinkintumėte." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index 4a5da4f20b..5665bbdc6d 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1,6 +1,7 @@ { "@@locale ": "en", "enterYourEmailAddress": "Voer je e-mailadres in", + "enterYourNewEmailAddress": "Voer uw nieuwe e-mailadres in", "accountWelcomeBack": "Welkom terug!", "emailAlreadyRegistered": "E-mail is al geregistreerd.", "emailNotRegistered": "E-mail niet geregistreerd.", @@ -371,6 +372,21 @@ "deleteFromBoth": "Verwijder van beide", "newAlbum": "Nieuw album", "albums": "Albums", + "memoryCount": "{count, plural, =0{geen herinneringen} one{{formattedCount} herinnering} other{{formattedCount} herinneringen}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} geselecteerd", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -706,6 +722,7 @@ "type": "text" }, "backupFailed": "Back-up mislukt", + "sorryBackupFailedDesc": "Sorry, we kunnen dit bestand nu niet back-uppen, we zullen het later opnieuw proberen.", "couldNotBackUpTryLater": "We konden uw gegevens niet back-uppen.\nWe zullen het later opnieuw proberen.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft", "pleaseGrantPermissions": "Geef alstublieft toestemming", @@ -777,6 +794,14 @@ "share": "Delen", "unhideToAlbum": "Zichtbaar maken in album", "restoreToAlbum": "Terugzetten naar album", + "moveItem": "{count, plural, =1 {Bestand verplaatsen} other {Bestanden verplaatsen}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Bestand toevoegen} other {Bestanden toevoegen}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Maak of selecteer album", "selectAlbum": "Album selecteren", "searchByAlbumNameHint": "Albumnaam", @@ -874,6 +899,7 @@ "authToViewYourMemories": "Graag verifiëren om uw herinneringen te bekijken", "unlock": "Ontgrendelen", "freeUpSpace": "Ruimte vrijmaken", + "freeUpSpaceSaving": "{count, plural, =1 {Het kan verwijderd worden van het apparaat om {formattedSize} vrij te maken} other {Ze kunnen verwijderd worden van het apparaat om {formattedSize} vrij te maken}}", "filesBackedUpInAlbum": "{count, plural, one {1 bestand} other {{formattedNumber} bestanden}} in dit album is veilig geback-upt", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +930,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Je hebt nog steeds toegang tot {count, plural, =1 {het} other {ze}} op Ente zolang je een actief abonnement hebt", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "Maak {sizeInMBorGB} vrij", "thisEmailIsAlreadyInUse": "Dit e-mailadres is al in gebruik", "incorrectCode": "Onjuiste code", @@ -993,7 +1031,7 @@ "didYouKnow": "Wist u dat?", "loadingMessage": "Uw foto's laden...", "loadMessage1": "U kunt uw abonnement met uw familie delen", - "loadMessage2": "We hebben tot nu toe meer dan tien miljoen herinneringen bewaard", + "loadMessage2": "We hebben tot nu toe meer dan 200 miljoen herinneringen bewaard", "loadMessage3": "We bewaren 3 kopieën van uw bestanden, één in een ondergrondse kernbunker", "loadMessage4": "Al onze apps zijn open source", "loadMessage5": "Onze broncode en cryptografie zijn extern gecontroleerd en geverifieerd", @@ -1231,6 +1269,8 @@ "description": "Subtitle to indicate that the user can find people quickly by name" }, "findPeopleByName": "Mensen snel op naam zoeken", + "addViewers": "{count, plural, =0 {Kijker toevoegen} =1 {Kijker toevoegen} other {Kijkers toevoegen}}", + "addCollaborators": "{count, plural, =0 {Samenwerker toevoegen} =1 {Samenwerker toevoegen} other {Samenwerkers toevoegen}}", "longPressAnEmailToVerifyEndToEndEncryption": "Druk lang op een e-mail om de versleuteling te verifiëren.", "developerSettingsWarning": "Weet je zeker dat je de ontwikkelaarsinstellingen wilt wijzigen?", "developerSettings": "Ontwikkelaarsinstellingen", @@ -1242,6 +1282,8 @@ "createCollaborativeLink": "Maak een gezamenlijke link", "search": "Zoeken", "enterPersonName": "Naam van persoon invoeren", + "editEmailAlreadyLinked": "Dit e-mailadres is al aan {name} gekoppeld.", + "viewPersonToUnlink": "Bekijk {name} om te ontkoppelen", "enterName": "Naam invoeren", "savePerson": "Persoon opslaan", "editPerson": "Persoon bewerken", @@ -1362,6 +1404,16 @@ "enableMachineLearningBanner": "Schakel machine learning in voor magische zoekopdrachten en gezichtsherkenning", "searchDiscoverEmptySection": "Afbeeldingen worden hier getoond zodra verwerking en synchroniseren voltooid is", "searchPersonsEmptySection": "Personen worden hier getoond zodra verwerking en synchroniseren voltooid is", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 kijkers toegevoegd} =1 {1 kijker toegevoegd} other {{count} kijkers toegevoegd}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 samenwerkers toegevoegd} =1 {1 samenwerker toegevoegd} other {{count} samenwerkers toegevoegd}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1437,6 +1489,15 @@ }, "currentlyRunning": "momenteel bezig", "ignored": "genegeerd", + "photosCount": "{count, plural, =0 {0 foto's} =1 {1 foto} other {{count} foto's}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Bestand", "searchSectionsLengthMismatch": "Lengte van secties komt niet overeen: {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1602,7 +1663,7 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video streamen", + "videoStreaming": "Video's met stream", "processingVideos": "Video's verwerken", "streamDetails": "Stream details", "processing": "Verwerken", @@ -1669,14 +1730,26 @@ "onTheRoad": "Onderweg", "food": "Culinaire vreugde", "pets": "Harige kameraden", - "cLIcon": "Nieuw icoon", - "cLIconDesc": "Tot slot, een nieuwe app icoon waarvan we denken dat het ons werk het beste weergeeft. We hebben ook een icon-switcher toegevoegd zodat je het oude icoon kunt blijven gebruiken.", - "cLMemories": "Herinneringen", - "cLMemoriesDesc": "Ontdek nog eens je speciale momenten - spotlicht op je favoriete mensen, op je reizen en vakanties, op de beste plaatjes, en nog veel meer. Zet Machine Learning aan, tag jezelf en benoem je vrienden voor de beste ervaring.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Widgets die zijn geïntegreerd met herinneringen zijn nu beschikbaar. Ze tonen je speciale momenten zonder de app te openen.", - "cLFamilyPlan": "Familieplan limieten", - "cLFamilyPlanDesc": "Je kunt nu limieten instellen voor hoeveel opslag je familieleden kunnen gebruiken.", - "cLBulkEdit": "Bulk datums wijzigen", - "cLBulkEditDesc": "Je kunt nu meerdere foto's selecteren en de datum/tijd van ze allemaal bewerken met één snelle actie. Verschuiven van datums wordt ook ondersteund." + "curatedMemories": "Samengestelde herinneringen", + "widgets": "Widgets", + "memories": "Herinneringen", + "peopleWidgetDesc": "Selecteer de mensen die je wilt zien op je beginscherm.", + "albumsWidgetDesc": "Selecteer de albums die je wilt zien op je beginscherm.", + "memoriesWidgetDesc": "Selecteer het soort herinneringen dat je wilt zien op je beginscherm.", + "smartMemories": "Slimme herinneringen", + "pastYearsMemories": "Afgelopen jaren", + "deleteMultipleAlbumDialog": "Verwijder de foto's (en video's) van deze {count} albums ook uit alle andere albums waar deze deel van uitmaken?", + "addParticipants": "Voeg deelnemers toe", + "selectedAlbums": "{count} geselecteerd", + "actionNotSupportedOnFavouritesAlbum": "Actie niet ondersteund op Favorieten album", + "onThisDayMemories": "Op deze dag herinneringen", + "onThisDay": "Op deze dag", + "lookBackOnYourMemories": "Kijk terug op je herinneringen 🌄", + "newPhotosEmoji": " nieuwe 📸", + "sorryWeHadToPauseYourBackups": "Sorry, we moesten je back-ups pauzeren", + "clickToInstallOurBestVersionYet": "Klik om onze beste versie tot nu toe te installeren", + "onThisDayNotificationExplanation": "Ontvang meldingen over herinneringen op deze dag door de jaren heen.", + "addMemoriesWidgetPrompt": "Voeg een widget toe aan je beginscherm en kom hier terug om aan te passen.", + "addAlbumWidgetPrompt": "Voeg een widget toe aan je beginscherm en kom hier terug om aan te passen.", + "addPeopleWidgetPrompt": "Voeg een widget toe aan je beginscherm en kom hier terug om aan te passen." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index f05187bf2a..b5c93dab92 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -371,6 +371,21 @@ "deleteFromBoth": "Slett fra begge", "newAlbum": "Nytt album", "albums": "Album", + "memoryCount": "{count, plural, =0{ingen minner} one{{formattedCount} minne} other{{formattedCount} minner}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} valgt", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -777,6 +792,14 @@ "share": "Del", "unhideToAlbum": "Gjør synlig i album", "restoreToAlbum": "Gjenopprett til album", + "moveItem": "{count, plural, =1 {Flytt elementet} other {Flytt elementene}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural, =1 {Legg til element} other {Legg til elementene}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Opprett eller velg album", "selectAlbum": "Velg album", "searchByAlbumNameHint": "Albumnavn", @@ -993,7 +1016,6 @@ "didYouKnow": "Visste du at?", "loadingMessage": "Laster bildene dine...", "loadMessage1": "Du kan dele abonnementet med familien din", - "loadMessage2": "Vi har bevart over 30 millioner minner så langt", "loadMessage3": "Vi beholder 3 kopier av dine data, en i en underjordisk bunker", "loadMessage4": "Alle våre apper har åpen kildekode", "loadMessage5": "Vår kildekode og kryptografi har blitt revidert eksternt", @@ -1362,7 +1384,7 @@ "enableMachineLearningBanner": "Aktiver maskinlæring for magisk søk og ansiktsgjenkjenning", "searchDiscoverEmptySection": "Bilder vil vises her når behandlingen og synkronisering er fullført", "searchPersonsEmptySection": "Folk vil vises her når behandling og synkronisering er fullført", - "collaboratorsSuccessfullyAdded": "{count, plural, one {}=0 {La til 0 samarbeidspartner} =1 {La til 1 samarbeidspartner} other {Lagt til {count} samarbeidspartnere}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {La til 0 samarbeidspartner} =1 {La til 1 samarbeidspartner} other {Lagt til {count} samarbeidspartnere}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1602,7 +1624,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Videostrømming", "processingVideos": "Behandler videoer", "streamDetails": "Strømmedetaljer", "processing": "Behandler", @@ -1668,15 +1689,5 @@ "moon": "I månelyset", "onTheRoad": "På veien igjen", "food": "Kulinær glede", - "pets": "Pelsvenner", - "cLIcon": "Nytt ikon", - "cLIconDesc": "Endelig er et nytt appikon, som vi tror best representerer arbeidet vårt. Vi har også lagt til en icon-switcher slik at du kan fortsette å bruke det gamle ikonet.", - "cLMemories": "Minner", - "cLMemoriesDesc": "Gjenoppdag dine spesielle øyeblikk - fremhev dine favorittpersoner, dine turer og ferier, de beste bildene dine, og mye mer. Skru på maskinlæring, merk deg selv og navngi vennene dine for best mulig opplevelse.", - "cLWidgets": "Widgeter", - "cLWidgetsDesc": "Hjemmeskjermwidgeter som er integrert med minner er nå tilgjengelige. De vil vise dine spesielle øyeblikk uten å åpne appen.", - "cLFamilyPlan": "Begrensninger for familieabonnement", - "cLFamilyPlanDesc": "Du kan nå sette grenser for hvor mye lagringsplass familiemedlemmer kan bruke.", - "cLBulkEdit": "Masseendring av datoer", - "cLBulkEditDesc": "Du kan nå velge flere bilder, og redigere dato/klokkeslett for alle med en rask handling. Forskyving av datoer støttes også." + "pets": "Pelsvenner" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index 4f1c0e00bb..6181cdb0a8 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -992,7 +992,6 @@ "didYouKnow": "Czy wiedziałeś/aś?", "loadingMessage": "Wczytywanie Twoich zdjęć...", "loadMessage1": "Możesz udostępnić swoją subskrypcję swojej rodzinie", - "loadMessage2": "Do tej pory zachowaliśmy ponad 30 milionów wspomnień", "loadMessage3": "Przechowujemy 3 kopie Twoich danych, jedną w podziemnym schronie", "loadMessage4": "Wszystkie nasze aplikacje są otwarto źródłowe", "loadMessage5": "Nasz kod źródłowy i kryptografia zostały poddane zewnętrznemu audytowi", diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 4c8b5f3eba..a85b5b1cd2 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -1,61 +1,59 @@ { "@@locale ": "en", - "enterYourEmailAddress": "Insira seu endereço de e-mail", - "accountWelcomeBack": "Bem-vindo(a) de volta!", - "emailAlreadyRegistered": "E-mail já registrado.", - "emailNotRegistered": "E-mail não registrado.", - "email": "E-mail", + "enterYourEmailAddress": "Insira o seu endereço de email", + "accountWelcomeBack": "Bem-vindo de volta!", + "email": "Email", "cancel": "Cancelar", "verify": "Verificar", - "invalidEmailAddress": "Endereço de e-mail inválido", - "enterValidEmail": "Insira um endereço de e-mail válido.", - "deleteAccount": "Excluir conta", - "askDeleteReason": "Por que você quer excluir sua conta?", - "deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar.", - "feedback": "Feedback", - "kindlyHelpUsWithThisInformation": "Ajude-nos com esta informação", - "confirmDeletePrompt": "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos.", - "confirmAccountDeletion": "Confirmar exclusão da conta", + "invalidEmailAddress": "Endereço de email inválido", + "enterValidEmail": "Por favor, insira um endereço de email válido.", + "deleteAccount": "Eliminar conta", + "askDeleteReason": "Qual o principal motivo pelo qual está a eliminar a conta?", + "deleteAccountFeedbackPrompt": "Lamentamos a sua partida. Indique-nos a razão para podermos melhorar o serviço.", + "feedback": "Opinião", + "kindlyHelpUsWithThisInformation": "Por favor, ajude-nos com esta informação", + "confirmDeletePrompt": "Sim, pretendo apagar permanentemente esta conta e os respetivos dados em todas as aplicações.", + "confirmAccountDeletion": "Confirmar eliminação de conta", "deleteAccountPermanentlyButton": "Excluir conta permanentemente", - "yourAccountHasBeenDeleted": "Sua conta foi excluída", - "selectReason": "Diga o motivo", - "deleteReason1": "Está faltando um recurso-chave que eu preciso", - "deleteReason2": "O aplicativo ou um certo recurso não funciona da maneira que eu acredito que deveria funcionar", - "deleteReason3": "Encontrei outro serviço que considero melhor", - "deleteReason4": "Meu motivo não está listado", - "sendEmail": "Enviar e-mail", - "deleteRequestSLAText": "Sua solicitação será revisada em até 72 horas.", - "deleteEmailRequest": "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado.", + "yourAccountHasBeenDeleted": "A sua conta foi eliminada", + "selectReason": "Selecionar motivo", + "deleteReason1": "Falta uma funcionalidade-chave de que eu necessito", + "deleteReason2": "O aplicativo ou um determinado recurso não se comportou como era suposto", + "deleteReason3": "Encontrei outro serviço de que gosto mais", + "deleteReason4": "O motivo não está na lista", + "sendEmail": "Enviar email", + "deleteRequestSLAText": "O seu pedido será processado dentro de 72 horas.", + "deleteEmailRequest": "Envie um e-mail para accountt-deletion@ente.io a partir do seu endereço de email registrado.", "entePhotosPerm": "Ente precisa de permissão para preservar suas fotos", - "ok": "OK", + "ok": "Ok", "createAccount": "Criar conta", "createNewAccount": "Criar nova conta", - "password": "Senha", - "confirmPassword": "Confirmar senha", + "password": "Palavra-passe", + "confirmPassword": "Confirmar palavra-passe", "activeSessions": "Sessões ativas", - "oops": "Ops", - "somethingWentWrongPleaseTryAgain": "Algo deu errado. Tente outra vez", - "thisWillLogYouOutOfThisDevice": "Isso fará você sair deste dispositivo!", - "thisWillLogYouOutOfTheFollowingDevice": "Isso fará você sair do dispositivo a seguir:", - "terminateSession": "Sair?", - "terminate": "Encerrar", + "oops": "Oops", + "somethingWentWrongPleaseTryAgain": "Ocorreu um erro. Tente novamente", + "thisWillLogYouOutOfThisDevice": "Irá desconectar a sua conta do seu dispositivo!", + "thisWillLogYouOutOfTheFollowingDevice": "Irá desconectar a sua conta do seguinte dispositivo:", + "terminateSession": "Terminar sessão?", + "terminate": "Terminar", "thisDevice": "Este dispositivo", "recoverButton": "Recuperar", - "recoverySuccessful": "Recuperação com sucesso!", - "decrypting": "Descriptografando...", + "recoverySuccessful": "Recuperação bem sucedida!", + "decrypting": "A desencriptar…", "incorrectRecoveryKeyTitle": "Chave de recuperação incorreta", "incorrectRecoveryKeyBody": "A chave de recuperação inserida está incorreta", - "forgotPassword": "Esqueci a senha", - "enterYourRecoveryKey": "Insira sua chave de recuperação", - "noRecoveryKey": "Sem chave de recuperação?", + "forgotPassword": "Esqueceu-se da palavra-passe", + "enterYourRecoveryKey": "Insira a sua chave de recuperação", + "noRecoveryKey": "Não tem chave de recuperação?", "sorry": "Desculpe", - "noRecoveryKeyNoDecryption": "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, seus dados não podem ser descriptografados sem sua senha ou chave de recuperação", - "verifyEmail": "Verificar e-mail", - "toResetVerifyEmail": "Para redefinir sua senha, verifique seu e-mail primeiramente.", - "checkInboxAndSpamFolder": "Verifique sua caixa de entrada (e spam) para concluir a verificação", + "noRecoveryKeyNoDecryption": "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, os seus dados não podem ser descriptografados sem a sua palavra-passe ou a sua chave de recuperação", + "verifyEmail": "Verificar email", + "toResetVerifyEmail": "Para redefinir a sua palavra-passe, verifique primeiro o seu e-mail.", + "checkInboxAndSpamFolder": "Verifique a sua caixa de entrada (e spam) para concluir a verificação", "tapToEnterCode": "Toque para inserir código", "resendEmail": "Reenviar e-mail", - "weHaveSendEmailTo": "Enviamos um e-mail à {email}", + "weHaveSendEmailTo": "Enviamos um e-mail para {email}", "@weHaveSendEmailTo": { "description": "Text to indicate that we have sent a mail to the user", "placeholders": { @@ -66,17 +64,17 @@ } } }, - "setPasswordTitle": "Definir senha", - "changePasswordTitle": "Alterar senha", - "resetPasswordTitle": "Redefinir senha", - "encryptionKeys": "Chaves de criptografia", - "passwordWarning": "Nós não armazenamos esta senha, se você esquecer, nós não poderemos descriptografar seus dados", - "enterPasswordToEncrypt": "Insira uma senha que podemos usar para criptografar seus dados", - "enterNewPasswordToEncrypt": "Insira uma senha nova para criptografar seus dados", + "setPasswordTitle": "Definir palavra-passe", + "changePasswordTitle": "Alterar palavra-passe", + "resetPasswordTitle": "Redefinir palavra-passe", + "encryptionKeys": "Chaves de encriptação", + "passwordWarning": "Não armazenamos esta palavra-passe, se você a esquecer, não podemos desencriptar os seus dados", + "enterPasswordToEncrypt": "Inserir uma palavra-passe para encriptar os seus dados", + "enterNewPasswordToEncrypt": "Inserir uma nova palavra-passe para encriptar os seus dados", "weakStrength": "Fraca", "strongStrength": "Forte", - "moderateStrength": "Moderado", - "passwordStrength": "Força da senha: {passwordStrengthValue}", + "moderateStrength": "Moderada", + "passwordStrength": "Força da palavra-passe: {passwordStrengthValue}", "@passwordStrength": { "description": "Text to indicate the password strength", "placeholders": { @@ -88,39 +86,39 @@ }, "message": "Password Strength: {passwordStrengthText}" }, - "passwordChangedSuccessfully": "Senha alterada com sucesso", - "generatingEncryptionKeys": "Gerando chaves de criptografia...", - "pleaseWait": "Aguarde...", + "passwordChangedSuccessfully": "Palavra-passe alterada com sucesso", + "generatingEncryptionKeys": "Gerando chaves de encriptação...", + "pleaseWait": "Por favor, aguarde ...", "continueLabel": "Continuar", "insecureDevice": "Dispositivo inseguro", - "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\ninicie sessão com um dispositivo diferente.", + "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Desculpe, não foi possível gerar chaves seguras neste dispositivo.\n\npor favor iniciar sessão com um dispositivo diferente.", "howItWorks": "Como funciona", - "encryption": "Criptografia", - "ackPasswordLostWarning": "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta.", - "privacyPolicyTitle": "Política de Privacidade", + "encryption": "Encriptação", + "ackPasswordLostWarning": "Eu entendo que se eu perder a minha palavra-passe, posso perder os meus dados já que esses dados são encriptados de ponta a ponta.", + "privacyPolicyTitle": "Política de privacidade", "termsOfServicesTitle": "Termos", - "signUpTerms": "Eu concordo com os termos de serviço e a política de privacidade", - "logInLabel": "Entrar", - "loginTerms": "Ao clicar em entrar, eu concordo com os termos de serviço e a política de privacidade", + "signUpTerms": "Eu concordo com os termos de serviço e política de privacidade", + "logInLabel": "Iniciar sessão", + "loginTerms": "Ao clicar em iniciar sessão, eu concordo com os termos de serviço e política de privacidade", "changeEmail": "Alterar e-mail", - "enterYourPassword": "Insira sua senha", + "enterYourPassword": "Introduza a sua palavra-passe", "welcomeBack": "Bem-vindo(a) de volta!", - "contactSupport": "Contatar suporte", - "incorrectPasswordTitle": "Senha incorreta", - "pleaseTryAgain": "Tente novamente", - "recreatePasswordTitle": "Redefinir senha", + "contactSupport": "Contactar o suporte", + "incorrectPasswordTitle": "Palavra-passe incorreta", + "pleaseTryAgain": "Por favor, tente novamente", + "recreatePasswordTitle": "Recriar palavra-passe", "useRecoveryKey": "Usar chave de recuperação", - "recreatePasswordBody": "O dispositivo atual não é poderoso o suficiente para verificar sua senha, no entanto, nós podemos regenerar numa maneira que funciona em todos os dispositivos.\n\nEntre usando a chave de recuperação e regenere sua senha (você pode usar a mesma novamente se desejar).", - "verifyPassword": "Verificar senha", + "recreatePasswordBody": "O dispositivo atual não é suficientemente poderoso para verificar a palavra-passe, mas podemos regenerar novamente de uma maneira que funcione no seu dispositivo.\n\nPor favor, iniciar sessão utilizando código de recuperação e gerar novamente a sua palavra-passe (pode utilizar a mesma se quiser).", + "verifyPassword": "Verificar palavra-passe", "recoveryKey": "Chave de recuperação", - "recoveryKeyOnForgotPassword": "Caso você esqueça sua senha, a única maneira de recuperar seus dados é com esta chave.", - "recoveryKeySaveDescription": "Não armazenamos esta chave, salve esta chave de 24 palavras em um lugar seguro.", - "doThisLater": "Fazer isso depois", - "saveKey": "Salvar chave", + "recoveryKeyOnForgotPassword": "Se esquecer sua palavra-passe, a única maneira de recuperar os seus dados é com esta chave.", + "recoveryKeySaveDescription": "Não armazenamos essa chave, por favor, guarde esta chave de 24 palavras num lugar seguro.", + "doThisLater": "Fazer isto mais tarde", + "saveKey": "Guardar chave", "recoveryKeyCopiedToClipboard": "Chave de recuperação copiada para a área de transferência", "recoverAccount": "Recuperar conta", "recover": "Recuperar", - "dropSupportEmail": "Envie um e-mail para {supportEmail} a partir do seu endereço de e-mail registrado", + "dropSupportEmail": "Envie um e-mail para {supportEmail} a partir do seu endereço de e-mail registado", "@dropSupportEmail": { "placeholders": { "supportEmail": { @@ -132,39 +130,39 @@ }, "twofactorSetup": "Configuração de dois fatores", "enterCode": "Insira o código", - "scanCode": "Escanear código", - "codeCopiedToClipboard": "Código copiado para a área de transferência", - "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole este código\npara o aplicativo autenticador", + "scanCode": "Ler código Qr", + "codeCopiedToClipboard": "Código copiado para área de transferência", + "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole este código\nno seu aplicativo de autenticação", "tapToCopy": "toque para copiar", - "scanThisBarcodeWithnyourAuthenticatorApp": "Escaneie este código de barras com\no aplicativo autenticador", - "enterThe6digitCodeFromnyourAuthenticatorApp": "Digite o código de 6 dígitos do\naplicativo de autenticador", + "scanThisBarcodeWithnyourAuthenticatorApp": "Leia este código com a sua aplicação dois fatores.", + "enterThe6digitCodeFromnyourAuthenticatorApp": "Introduzir o código de 6 dígitos da\nsua aplicação de autenticação", "confirm": "Confirmar", "setupComplete": "Configuração concluída", - "saveYourRecoveryKeyIfYouHaventAlready": "Salve sua chave de recuperação, se você ainda não fez", - "thisCanBeUsedToRecoverYourAccountIfYou": "Isso pode ser usado para recuperar sua conta se você perder seu segundo fator", + "saveYourRecoveryKeyIfYouHaventAlready": "Guarde a sua chave de recuperação, caso ainda não o tenha feito", + "thisCanBeUsedToRecoverYourAccountIfYou": "Isto pode ser usado para recuperar sua conta se você perder seu segundo fator", "twofactorAuthenticationPageTitle": "Autenticação de dois fatores", - "lostDevice": "Perdeu o dispositivo?", + "lostDevice": "Perdeu o seu dispositívo?", "verifyingRecoveryKey": "Verificando chave de recuperação...", "recoveryKeyVerified": "Chave de recuperação verificada", - "recoveryKeySuccessBody": "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança.", - "invalidRecoveryKey": "A chave de recuperação que você inseriu não é válida. Certifique-se de conter 24 caracteres, e verifique a ortografia de cada um deles.\n\nSe você inseriu um código de recuperação mais antigo, verifique se ele tem 64 caracteres e verifique cada um deles.", + "recoveryKeySuccessBody": "Ótimo! A sua chave de recuperação é válida. Obrigado por verificar.\n\nLembre-se de manter cópia de segurança da sua chave de recuperação.", + "invalidRecoveryKey": "A chave de recuperação que inseriu não é válida. Por favor, certifique-se que ela contém 24 palavras e verifique a ortografia de cada uma.\n\nSe inseriu um código de recuperação mais antigo, certifique-se de que tem 64 caracteres e verifique cada um deles.", "invalidKey": "Chave inválida", "tryAgain": "Tente novamente", "viewRecoveryKey": "Ver chave de recuperação", "confirmRecoveryKey": "Confirmar chave de recuperação", - "recoveryKeyVerifyReason": "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente.", - "confirmYourRecoveryKey": "Confirme sua chave de recuperação", + "recoveryKeyVerifyReason": "A sua chave de recuperação é a única forma de recuperar as suas fotografias se se esquecer da sua palavra-passe. Pode encontrar a sua chave de recuperação em Definições > Conta.\n\n\nIntroduza aqui a sua chave de recuperação para verificar se a guardou corretamente.", + "confirmYourRecoveryKey": "Confirmar chave de recuperação", "addViewer": "Adicionar visualizador", "addCollaborator": "Adicionar colaborador", "addANewEmail": "Adicionar um novo e-mail", - "orPickAnExistingOne": "Ou escolha um existente", + "orPickAnExistingOne": "Ou escolha um já existente", "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.", - "enterEmail": "Inserir e-mail", - "albumOwner": "Proprietário", + "enterEmail": "Digite o e-mail", + "albumOwner": "Dono", "@albumOwner": { "description": "Role of the album owner" }, - "you": "Você", + "you": "Tu", "collaborator": "Colaborador", "addMore": "Adicionar mais", "@addMore": { @@ -176,37 +174,35 @@ "@removeParticipant": { "description": "menuSectionTitle for removing a participant" }, - "manage": "Gerenciar", + "manage": "Gerir", "addedAs": "Adicionado como", - "changePermissions": "Alterar permissões?", + "changePermissions": "Alterar permissões", "yesConvertToViewer": "Sim, converter para visualizador", - "cannotAddMorePhotosAfterBecomingViewer": "{user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda conseguirão remover fotos existentes adicionadas por eles", + "cannotAddMorePhotosAfterBecomingViewer": "{user} não será capaz de adicionar mais fotos a este álbum\n\nEles ainda serão capazes de remover fotos existentes adicionadas por eles", "allowAddingPhotos": "Permitir adicionar fotos", "@allowAddingPhotos": { "description": "Switch button to enable uploading photos to a public link" }, - "allowAddPhotosDescription": "Permitir que as pessoas com link também adicionem fotos ao álbum compartilhado.", - "passwordLock": "Bloqueio por senha", - "canNotOpenTitle": "Não pôde abrir este álbum", - "canNotOpenBody": "Desculpe, este álbum não pode ser aberto no aplicativo.", - "disableDownloadWarningTitle": "Por favor, saiba que", - "disableDownloadWarningBody": "Os visualizadores podem fazer capturas de tela ou salvar uma cópia de suas fotos usando ferramentas externas", + "allowAddPhotosDescription": "Permitir que pessoas com o link também adicionem fotos ao álbum compartilhado.", + "passwordLock": "Bloqueio da palavra-passe", + "disableDownloadWarningTitle": "Por favor, observe", + "disableDownloadWarningBody": "Visualizadores ainda podem fazer capturas de tela ou salvar uma cópia das suas fotos usando ferramentas externas", "allowDownloads": "Permitir downloads", - "linkDeviceLimit": "Limite do dispositivo", + "linkDeviceLimit": "Limite de dispositivo", "noDeviceLimit": "Nenhum", "@noDeviceLimit": { "description": "Text to indicate that there is limit on number of devices" }, - "linkExpiry": "Expiração do link", + "linkExpiry": "Link expirado", "linkExpired": "Expirado", "linkEnabled": "Ativado", "linkNeverExpires": "Nunca", - "expiredLinkInfo": "O link expirou. Selecione um novo tempo de expiração ou desative a expiração do link.", - "setAPassword": "Definir senha", + "expiredLinkInfo": "Este link expirou. Por favor, selecione um novo tempo de expiração ou desabilite a expiração do link.", + "setAPassword": "Definir uma palavra-passe", "lockButtonLabel": "Bloquear", - "enterPassword": "Inserir senha", + "enterPassword": "Introduzir palavra-passe", "removeLink": "Remover link", - "manageLink": "Gerenciar link", + "manageLink": "Gerir link", "linkExpiresOn": "O link expirará em {expiryTime}", "albumUpdated": "Álbum atualizado", "never": "Nunca", @@ -214,12 +210,12 @@ "@custom": { "description": "Label for setting custom value for link expiry" }, - "after1Hour": "Após 1 hora", - "after1Day": "Após 1 dia", - "after1Week": "Após 1 semana", - "after1Month": "Após 1 mês", - "after1Year": "Após 1 ano", - "manageParticipants": "Gerenciar", + "after1Hour": "Depois de 1 Hora", + "after1Day": "Depois de 1 dia", + "after1Week": "Depois de 1 semana", + "after1Month": "Depois de 1 mês", + "after1Year": "Depois de 1 ano", + "manageParticipants": "Gerir", "albumParticipantsCount": "{count, plural, =0 {Nenhum participante} =1 {1 participante} other {{count} participantes}}", "@albumParticipantsCount": { "placeholders": { @@ -230,17 +226,17 @@ }, "description": "Number of participants in an album, including the album owner." }, - "collabLinkSectionDescription": "Crie um link para permitir que as pessoas adicionem e vejam fotos no seu álbum compartilhado sem a necessidade do aplicativo ou uma conta Ente. Ótimo para colecionar fotos de eventos.", + "collabLinkSectionDescription": "Criar um link para permitir que as pessoas adicionem e visualizem fotos em seu álbum compartilhado sem precisar de um aplicativo Ente ou conta. Ótimo para coletar fotos do evento.", "collectPhotos": "Coletar fotos", "collaborativeLink": "Link colaborativo", - "shareWithNonenteUsers": "Compartilhar com usuários não ente", + "shareWithNonenteUsers": "Compartilhar com usuários que não usam Ente", "createPublicLink": "Criar link público", "sendLink": "Enviar link", "copyLink": "Copiar link", "linkHasExpired": "O link expirou", - "publicLinkEnabled": "Link público ativo", - "shareALink": "Compartilhar link", - "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários Ente, incluindo usuários em planos gratuitos.", + "publicLinkEnabled": "Link público ativado", + "shareALink": "Partilhar um link", + "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos.", "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", "@shareWithPeopleSectionTitle": { "placeholders": { @@ -251,8 +247,8 @@ } }, "thisIsYourVerificationId": "Este é o seu ID de verificação", - "someoneSharingAlbumsWithYouShouldSeeTheSameId": "Alguém compartilhando álbuns com você deve ver o mesmo ID no dispositivo.", - "howToViewShareeVerificationID": "Peça-os para manterem pressionado no endereço de e-mail na tela de opções, e verifique-se os IDs de ambos os dispositivos correspondem.", + "someoneSharingAlbumsWithYouShouldSeeTheSameId": "Alguém compartilhando álbuns com você deve ver o mesmo ID no seu dispositivo.", + "howToViewShareeVerificationID": "Por favor, peça-lhes para pressionar longamente o endereço de e-mail na tela de configurações e verifique se os IDs de ambos os dispositivos coincidem.", "thisIsPersonVerificationId": "Este é o ID de verificação de {email}", "@thisIsPersonVerificationId": { "placeholders": { @@ -262,44 +258,44 @@ } } }, - "verificationId": "ID de verificação", - "verifyEmailID": "Verificar {email}", - "emailNoEnteAccount": "{email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos.", - "shareMyVerificationID": "Aqui está meu ID de verificação para o ente.io: {verificationID}", - "shareTextConfirmOthersVerificationID": "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: {verificationID}", - "somethingWentWrong": "Algo deu errado", + "verificationId": "ID de Verificação", + "verifyEmailID": "Verificar e-mail", + "emailNoEnteAccount": "{email} não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos.", + "shareMyVerificationID": "Aqui está o meu ID de verificação: {verificationID} para ente.io.", + "shareTextConfirmOthersVerificationID": "Ei, você pode confirmar que este é seu ID de verificação do ente.io: {verificationID}", + "somethingWentWrong": "Ocorreu um erro", "sendInvite": "Enviar convite", - "shareTextRecommendUsingEnte": "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io", + "shareTextRecommendUsingEnte": "Descarregue o Ente para poder partilhar facilmente fotografias e vídeos de qualidade original\n\n\nhttps://ente.io", "done": "Concluído", "applyCodeTitle": "Aplicar código", - "enterCodeDescription": "Insira o código fornecido pelo seu amigo para reivindicar o armazenamento grátis para os dois", + "enterCodeDescription": "Introduza o código fornecido pelo seu amigo para obter armazenamento gratuito para ambos", "apply": "Aplicar", - "failedToApplyCode": "Falhou ao aplicar código", - "enterReferralCode": "Inserir código de referência", + "failedToApplyCode": "Falha ao aplicar código", + "enterReferralCode": "Insira o código de referência", "codeAppliedPageTitle": "Código aplicado", - "changeYourReferralCode": "Alterar código de referência", + "changeYourReferralCode": "Alterar o código de referência", "change": "Alterar", - "unavailableReferralCode": "Desculpe, este código está indisponível.", - "codeChangeLimitReached": "Desculpe, você atingiu o limite de mudanças de código.", + "unavailableReferralCode": "Desculpe, este código não está disponível.", + "codeChangeLimitReached": "Desculpe, você atingiu o limite de alterações de código.", "onlyFamilyAdminCanChangeCode": "Entre em contato com {familyAdminEmail} para alterar o seu código.", "storageInGB": "{storageAmountInGB} GB", - "claimed": "Reivindicado", + "claimed": "Reclamado", "@claimed": { "description": "Used to indicate storage claimed, like 10GB Claimed" }, "details": "Detalhes", - "claimMore": "Reivindique mais!", + "claimMore": "Reclamar mais!", "theyAlsoGetXGb": "Eles também recebem {storageAmountInGB} GB", - "freeStorageOnReferralSuccess": "{storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código", - "shareTextReferralCode": "Código de referência do Ente: {referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter {referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io", - "claimFreeStorage": "Reivindicar armazenamento grátis", - "inviteYourFriends": "Convide seus amigos", - "failedToFetchReferralDetails": "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde.", + "freeStorageOnReferralSuccess": "{storageAmountInGB} GB sempre que alguém se inscreve num plano pago e aplica o seu código", + "shareTextReferralCode": "Insira o código de referência: {referralCode} \n\nAplique-o em Configurações → Geral → Indicações para obter {referralStorageInGB} GB gratuitamente após a sua inscrição para um plano pago\n\nhttps://ente.io", + "claimFreeStorage": "Solicitar armazenamento gratuito", + "inviteYourFriends": "Convide os seus amigos", + "failedToFetchReferralDetails": "Não foi possível obter detalhes de indicação. Por favor, tente novamente mais tarde.", "referralStep1": "1. Envie este código aos seus amigos", - "referralStep2": "2. Eles então se inscrevem num plano pago", - "referralStep3": "3. Ambos os dois ganham {storageInGB} GB* grátis", - "referralsAreCurrentlyPaused": "As referências estão atualmente pausadas", - "youCanAtMaxDoubleYourStorage": "* Você pode duplicar seu armazenamento ao máximo", + "referralStep2": "2. Eles se inscrevem em um plano pago", + "referralStep3": "3. Ambos ganham {storageInGB} GB* grátis", + "referralsAreCurrentlyPaused": "As referências estão atualmente em pausa", + "youCanAtMaxDoubleYourStorage": "* Você pode duplicar seu armazenamento no máximo", "claimedStorageSoFar": "{isFamilyMember, select,true {Sua família reinvidicou {storageAmountInGb} GB até então}false {Você reinvindicou {storageAmountInGb} GB até então}other {Você reinvindicou {storageAmountInGb} GB até então!}}", "@claimedStorageSoFar": { "placeholders": { @@ -313,17 +309,17 @@ } } }, - "faq": "Perguntas frequentes", + "faq": "Perguntas Frequentes", "help": "Ajuda", "oopsSomethingWentWrong": "Ops, algo deu errado", - "peopleUsingYourCode": "Pessoas que usam seu código", + "peopleUsingYourCode": "Pessoas que utilizam seu código", "eligible": "elegível", "total": "total", "codeUsedByYou": "Código usado por você", - "freeStorageClaimed": "Armazenamento grátis reivindicado", - "freeStorageUsable": "Armazenamento disponível", + "freeStorageClaimed": "Armazenamento gratuito reclamado", + "freeStorageUsable": "Armazenamento livre utilizável", "usableReferralStorageInfo": "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano.", - "removeFromAlbumTitle": "Remover do álbum?", + "removeFromAlbumTitle": "Remover do álbum", "removeFromAlbum": "Remover do álbum", "itemsWillBeRemovedFromAlbum": "Os itens selecionados serão removidos deste álbum", "removeShareItemsWarning": "Alguns dos itens que você está removendo foram adicionados por outras pessoas, e você perderá o acesso a eles", @@ -331,61 +327,45 @@ "removingFromFavorites": "Removendo dos favoritos...", "sorryCouldNotAddToFavorites": "Desculpe, não foi possível adicionar aos favoritos!", "sorryCouldNotRemoveFromFavorites": "Desculpe, não foi possível remover dos favoritos!", - "subscribeToEnableSharing": "Você precisa de uma inscrição paga ativa para ativar o compartilhamento.", - "subscribe": "Inscrever-se", - "canOnlyRemoveFilesOwnedByYou": "Só pode remover arquivos de sua propriedade", + "subscribeToEnableSharing": "Você precisa de uma assinatura paga ativa para ativar o compartilhamento.", + "subscribe": "Subscrever", + "canOnlyRemoveFilesOwnedByYou": "", "deleteSharedAlbum": "Excluir álbum compartilhado?", - "deleteAlbum": "Excluir álbum", - "deleteAlbumDialog": "Também excluir as fotos (e vídeos) presentes neste álbum de todos os outros álbuns que eles fazem parte?", - "deleteSharedAlbumDialogBody": "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros", - "yesRemove": "Sim, excluir", - "creatingLink": "Criando link...", + "deleteAlbum": "Apagar álbum", + "deleteAlbumDialog": "Eliminar também as fotos (e vídeos) presentes neste álbum de all os outros álbuns de que fazem parte?", + "deleteSharedAlbumDialogBody": "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que são propriedade de outros", + "yesRemove": "Sim, remover", + "creatingLink": "Criar link...", "removeWithQuestionMark": "Remover?", - "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum", + "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por elas também serão removidas do álbum", "keepPhotos": "Manter fotos", - "deletePhotos": "Excluir fotos", - "inviteToEnte": "Convidar ao Ente", + "deletePhotos": "Apagar fotos", + "inviteToEnte": "Convidar para Ente", "removePublicLink": "Remover link público", - "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", - "sharing": "Compartilhando...", - "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivo", - "createAlbumActionHint": "Pressione para selecionar fotos e clique em + para criar um álbum", - "importing": "Importando....", - "failedToLoadAlbums": "Falhou ao carregar álbuns", + "disableLinkMessage": "Isto removerá o link público para acessar \"{albumName}\".", + "sharing": "Partilhar...", + "youCannotShareWithYourself": "Não podes partilhar contigo mesmo", + "archive": "............", + "createAlbumActionHint": "Pressione e segure para selecionar fotos e clique em + para criar um álbum", + "importing": "A importar...", + "failedToLoadAlbums": "Falha ao carregar álbuns", "hidden": "Oculto", - "authToViewYourHiddenFiles": "Autentique-se para visualizar seus arquivos ocultos", - "authToViewTrashedFiles": "Autentique-se para ver seus arquivos excluídos", - "trash": "Lixeira", + "authToViewYourHiddenFiles": "Por favor, autentique para ver seus arquivos ocultos", + "trash": "Lixo", "uncategorized": "Sem categoria", "videoSmallCase": "vídeo", "photoSmallCase": "foto", - "singleFileDeleteHighlight": "Ele será excluído de todos os álbuns.", - "singleFileInBothLocalAndRemote": "Este {fileType} está no Ente e em seu dispositivo.", - "singleFileInRemoteOnly": "Este {fileType} será excluído do Ente.", - "singleFileDeleteFromDevice": "Este {fileType} será excluído do dispositivo.", - "deleteFromEnte": "Excluir do Ente", - "yesDelete": "Sim, excluir", - "movedToTrash": "Movido para a lixeira", - "deleteFromDevice": "Excluir do dispositivo", - "deleteFromBoth": "Excluir de ambos", + "singleFileDeleteHighlight": "Será eliminado de todos os álbuns.", + "singleFileInBothLocalAndRemote": "Este {fileType} encontra-se tanto no Ente como no seu dispositivo.", + "singleFileInRemoteOnly": "Este {fileType} será eliminado do Ente.", + "singleFileDeleteFromDevice": "Este {fileType} será eliminado do seu dispositivo.", + "deleteFromEnte": "Apagar do Ente", + "yesDelete": "Sim, apagar", + "movedToTrash": "Mover para o lixo", + "deleteFromDevice": "Apagar do dispositivo", + "deleteFromBoth": "Apagar de ambos", "newAlbum": "Novo álbum", "albums": "Álbuns", - "memoryCount": "{count, plural, zero{sem memórias}\none{{formattedCount} memória}\nother{{formattedCount} memórias}}", - "@memoryCount": { - "description": "The text to display the number of memories", - "type": "text", - "placeholders": { - "count": { - "example": "1", - "type": "int" - }, - "formattedCount": { - "type": "String", - "example": "11.513, 11,511" - } - } - }, "selectedPhotos": "{count} selecionado(s)", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -412,31 +392,30 @@ } } }, - "advancedSettings": "Avançado", + "advancedSettings": "Definições avançadas", "@advancedSettings": { "description": "The text to display in the advanced settings section" }, - "photoGridSize": "Tamanho da grade de fotos", - "manageDeviceStorage": "Gerenciar cache do dispositivo", + "photoGridSize": "Tamanho da grelha de fotos", "manageDeviceStorageDesc": "Reveja e limpe o armazenamento de cache local.", "machineLearning": "Aprendizagem automática", "mlConsent": "Ativar aprendizagem automática", "mlConsentTitle": "Ativar aprendizagem automática?", - "mlConsentDescription": "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta.", - "mlConsentPrivacy": "Clique aqui para mais detalhes sobre este recurso na política de privacidade", + "mlConsentDescription": "Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.\n\n\nIsto acontecerá no seu dispositivo e todas as informações biométricas geradas serão encriptadas de ponta a ponta.", + "mlConsentPrivacy": "Por favor, clique aqui para mais detalhes sobre este recurso na nossa política de privacidade", "mlConsentConfirmation": "Eu entendo, e desejo ativar a aprendizagem automática", - "magicSearch": "Busca mágica", - "discover": "Explorar", + "magicSearch": "Pesquisa mágica", + "discover": "Descobrir", "@discover": { "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." }, "discover_identity": "Identidade", - "discover_screenshots": "Capturas de tela", + "discover_screenshots": "Capturas de ecrã", "discover_receipts": "Recibos", "discover_notes": "Notas", "discover_memes": "Memes", "discover_visiting_cards": "Cartões de visita", - "discover_babies": "Bebês", + "discover_babies": "Bebés", "discover_pets": "Animais de estimação", "discover_selfies": "Selfies", "discover_wallpapers": "Papéis de parede", @@ -445,21 +424,21 @@ "discover_sunset": "Pôr do sol", "discover_hills": "Colinas", "discover_greenery": "Vegetação", - "mlIndexingDescription": "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.", - "loadingModel": "Baixando modelos...", + "mlIndexingDescription": "Tenha em atenção que a aprendizagem automática resultará numa maior utilização da largura de banda e da bateria até que todos os itens sejam indexados. Considere utilizar a aplicação de ambiente de trabalho para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.", + "loadingModel": "Transferindo modelos...", "waitingForWifi": "Aguardando Wi-Fi...", - "status": "Estado", + "status": "Status", "indexedItems": "Itens indexados", "pendingItems": "Itens pendentes", "clearIndexes": "Limpar índices", - "selectFoldersForBackup": "Selecionar pastas para copiar com segurança", - "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão criptografadas e armazenadas em copiadas com segurança", + "selectFoldersForBackup": "Selecionar pastas para cópia de segurança", + "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão encriptadas e guardadas como cópia de segurança", "unselectAll": "Desmarcar tudo", "selectAll": "Selecionar tudo", "skip": "Pular", "updatingFolderSelection": "Atualizando seleção de pasta...", "itemCount": "{count, plural, one{{count} item} other{{count} itens}}", - "deleteItemCount": "{count, plural, =1 {Excluir {count} item} other {Excluir {count} itens}}", + "deleteItemCount": "{count, plural, =1 {Apagar {count} item} other {Apagar {count} itens}}", "duplicateItemsGroup": "{count} arquivos, {formattedSize} cada", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -477,58 +456,57 @@ }, "showMemories": "Mostrar memórias", "yearsAgo": "{count, plural, one{{count} ano atrás} other{{count} anos atrás}}", - "backupSettings": "Opções de cópia de segurança", + "backupSettings": "Definições da cópia de segurança", "backupStatus": "Status da cópia de segurança", "backupStatusDescription": "Os itens que foram salvos com segurança aparecerão aqui", - "backupOverMobileData": "Salvamento com segurança usando dados móveis", + "backupOverMobileData": "Cópia de segurança através dos dados móveis", "backupVideos": "Cópia de segurança de vídeos", "disableAutoLock": "Desativar bloqueio automático", - "deviceLockExplanation": "Desativa o bloqueio de tela quando o Ente está de fundo e têm uma cópia de segurança sendo feita. Isso normalmente não é necessário, no entanto, ajuda a envios grandes e importações iniciais de bibliotecas maiores concluírem mais rápido.", + "deviceLockExplanation": "Desativar o bloqueio do ecrã do dispositivo quando o Ente estiver em primeiro plano e houver uma cópia de segurança em curso. Normalmente, isto não é necessário, mas pode ajudar a que os grandes carregamentos e as importações iniciais de grandes bibliotecas sejam concluídos mais rapidamente.", "about": "Sobre", "weAreOpenSource": "Nós somos de código aberto!", "privacy": "Privacidade", "terms": "Termos", - "checkForUpdates": "Buscar atualizações", - "checkStatus": "Verificar estado", - "checking": "Verificando...", - "youAreOnTheLatestVersion": "Você está na versão mais recente", + "checkForUpdates": "Procurar atualizações", + "checkStatus": "Verificar status", + "checking": "A verificar...", + "youAreOnTheLatestVersion": "Está a utilizar a versão mais recente", "account": "Conta", - "manageSubscription": "Gerenciar assinatura", + "manageSubscription": "Gerir subscrição", "authToChangeYourEmail": "Por favor, autentique-se para alterar o seu e-mail", - "changePassword": "Alterar senha", - "authToChangeYourPassword": "Autentique para alterar sua senha", + "changePassword": "Alterar palavra-passe", + "authToChangeYourPassword": "Por favor, autentique-se para alterar a palavra-passe", "emailVerificationToggle": "Verificação por e-mail", - "authToChangeEmailVerificationSetting": "Autentique-se para alterar o e-mail de verificação", - "exportYourData": "Exportar dados", - "logout": "Encerrar sessão", - "authToInitiateAccountDeletion": "Autentique para iniciar a exclusão de conta", - "areYouSureYouWantToLogout": "Você tem certeza que quer encerrar sessão?", - "yesLogout": "Sim, encerrar sessão", - "aNewVersionOfEnteIsAvailable": "Uma nova versão do Ente está disponível.", + "authToChangeEmailVerificationSetting": "Por favor, autentique-se para alterar a verificação de e-mail", + "exportYourData": "Exportar os seus dados", + "logout": "Terminar sessão", + "authToInitiateAccountDeletion": "Autentique-se para iniciar a eliminação da conta", + "areYouSureYouWantToLogout": "Tem certeza que deseja terminar a sessão?", + "yesLogout": "Sim, terminar sessão", + "aNewVersionOfEnteIsAvailable": "Está disponível uma nova versão do Ente.", "update": "Atualizar", "installManually": "Instalar manualmente", "criticalUpdateAvailable": "Atualização crítica disponível", "updateAvailable": "Atualização disponível", "ignoreUpdate": "Ignorar", - "downloading": "Baixando...", - "cannotDeleteSharedFiles": "Não é possível excluir arquivos compartilhados", - "theDownloadCouldNotBeCompleted": "A instalação não pôde ser concluída", + "downloading": "A transferir...", + "cannotDeleteSharedFiles": "Não é possível eliminar ficheiros partilhados", + "theDownloadCouldNotBeCompleted": "Não foi possível concluir o download.", "retry": "Tentar novamente", - "backedUpFolders": "Pastas copiadas com segurança", + "backedUpFolders": "Pastas com cópia de segurança", "backup": "Cópia de segurança", - "freeUpDeviceSpace": "Liberar espaço no dispositivo", - "freeUpDeviceSpaceDesc": "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança.", + "freeUpDeviceSpace": "Libertar espaço no dispositivo", + "freeUpDeviceSpaceDesc": "Poupe espaço no seu dispositivo limpando ficheiros dos quais já foi feita uma cópia de segurança.", "allClear": "✨ Tudo limpo", - "noDeviceThatCanBeDeleted": "Você não tem arquivos neste dispositivo que possam ser excluídos", - "removeDuplicates": "Excluir duplicatas", - "removeDuplicatesDesc": "Revise e remova arquivos que são duplicatas exatas.", - "viewLargeFiles": "Arquivos grandes", - "viewLargeFilesDesc": "Ver arquivos que consumem a maior parte do armazenamento.", - "noDuplicates": "✨ Sem duplicatas", - "youveNoDuplicateFilesThatCanBeCleared": "Você não possui nenhum arquivo duplicado que possa ser excluído", + "noDeviceThatCanBeDeleted": "Você não tem arquivos neste dispositivo que possam ser apagados", + "removeDuplicates": "Remover duplicados", + "removeDuplicatesDesc": "Rever e remover ficheiros que sejam duplicados exatos.", + "viewLargeFiles": "Ficheiros grandes", + "viewLargeFilesDesc": "Ver os ficheiros que estão a consumir a maior quantidade de armazenamento.", + "noDuplicates": "✨ Sem duplicados", "success": "Sucesso", - "rateUs": "Avaliar", - "remindToEmptyDeviceTrash": "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado", + "rateUs": "Avalie-nos", + "remindToEmptyDeviceTrash": "Esvazie também a opção “Eliminados recentemente” em “Definições” -> “Armazenamento” para reclamar o espaço libertado", "youHaveSuccessfullyFreedUp": "Você liberou {storageSaved} com sucesso!", "@youHaveSuccessfullyFreedUp": { "description": "The text to display when the user has successfully freed up storage", @@ -540,9 +518,9 @@ } } }, - "remindToEmptyEnteTrash": "Também esvazie sua \"Lixeira\" para reivindicar o espaço liberado", + "remindToEmptyEnteTrash": "Esvazie também o seu “Lixo” para reivindicar o espaço libertado", "sparkleSuccess": "✨ Sucesso", - "duplicateFileCountWithStorageSaved": "Você limpou {count, plural, one{{count} arquivo duplicado} other{{count} arquivos duplicados}}, salvando ({storageSaved}!)", + "duplicateFileCountWithStorageSaved": "Você limpou {count, plural, one{{count} arquivo duplicado} other{{count} arquivos duplicados}}, guardando ({storageSaved}!)", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -560,43 +538,43 @@ "familyPlans": "Planos familiares", "referrals": "Referências", "notifications": "Notificações", - "sharedPhotoNotifications": "Novas fotos compartilhadas", - "sharedPhotoNotificationsExplanation": "Receber notificações quando alguém adicionar uma foto a um álbum compartilhado que você faz parte", + "sharedPhotoNotifications": "Novas fotos partilhadas", + "sharedPhotoNotificationsExplanation": "Receber notificações quando alguém adiciona uma foto a um álbum partilhado do qual faz parte", "advanced": "Avançado", "general": "Geral", "security": "Segurança", - "authToViewYourRecoveryKey": "Autentique para ver sua chave de recuperação", + "authToViewYourRecoveryKey": "Por favor, autentique-se para ver a chave de recuperação", "twofactor": "Dois fatores", - "authToConfigureTwofactorAuthentication": "Autentique para configurar a autenticação de dois fatores", - "lockscreen": "Tela de bloqueio", - "authToChangeLockscreenSetting": "Autentique para alterar a configuração da tela de bloqueio", + "authToConfigureTwofactorAuthentication": "Por favor, autentique para configurar a autenticação de dois fatores", + "lockscreen": "Ecrã de bloqueio", + "authToChangeLockscreenSetting": "Por favor, autentique-se para alterar a configuração da tela do ecrã de bloqueio", "viewActiveSessions": "Ver sessões ativas", - "authToViewYourActiveSessions": "Autentique para ver as sessões ativas", + "authToViewYourActiveSessions": "Por favor, autentique-se para ver as suas sessões ativas", "disableTwofactor": "Desativar autenticação de dois fatores", - "confirm2FADisable": "Você tem certeza que queira desativar a autenticação de dois fatores?", + "confirm2FADisable": "Tem a certeza de que pretende desativar a autenticação de dois fatores?", "no": "Não", "yes": "Sim", - "social": "Redes sociais", - "rateUsOnStore": "Avalie-nos no {storeName}", + "social": "Social", + "rateUsOnStore": "Avalie-nos em {storeName}", "blog": "Blog", "merchandise": "Produtos", - "twitter": "Twitter/X", + "twitter": "Twitter", "mastodon": "Mastodon", "matrix": "Matrix", "discord": "Discord", "reddit": "Reddit", - "yourStorageDetailsCouldNotBeFetched": "Seus detalhes de armazenamento não puderam ser obtidos", - "reportABug": "Informar um erro", - "reportBug": "Informar erro", - "suggestFeatures": "Sugerir recurso", + "yourStorageDetailsCouldNotBeFetched": "Não foi possível obter os seus dados de armazenamento", + "reportABug": "Reporte um bug", + "reportBug": "Reportar bug", + "suggestFeatures": "Sugerir recursos", "support": "Suporte", "theme": "Tema", "lightTheme": "Claro", "darkTheme": "Escuro", "systemTheme": "Sistema", - "freeTrial": "Avaliação grátis", - "selectYourPlan": "Selecione seu plano", - "enteSubscriptionPitch": "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo.", + "freeTrial": "Teste grátis", + "selectYourPlan": "Selecione o seu plano", + "enteSubscriptionPitch": "O Ente preserva as suas memórias, para que estejam sempre disponíveis, mesmo que perca o seu dispositivo.", "enteSubscriptionShareWithFamily": "Sua família também pode ser adicionada ao seu plano.", "currentUsageIs": "O uso atual é ", "@currentUsageIs": { @@ -607,22 +585,22 @@ "type": "text" }, "faqs": "Perguntas frequentes", - "renewsOn": "Renovação de assinatura em {endDate}", - "freeTrialValidTill": "A avaliação grátis acaba em {endDate}", + "renewsOn": "A subscrição é renovada em {endDate}", + "freeTrialValidTill": "Teste gratuito válido até {endDate}", "validTill": "Válido até {endDate}", - "addOnValidTill": "Seu complemento {storageAmount} é válido até {endDate}", - "playStoreFreeTrialValidTill": "Avaliação grátis válida até {endDate}.\nVocê pode alterar para um plano pago depois.", - "subWillBeCancelledOn": "Sua assinatura será cancelada em {endDate}", - "subscription": "Assinatura", + "addOnValidTill": "Seu addon {storageAmount} é válido até o momento {endDate}", + "playStoreFreeTrialValidTill": "Teste gratuito válido até {endDate}.\nVocê pode escolher um plano pago depois.", + "subWillBeCancelledOn": "A sua subscrição será cancelada em {endDate}", + "subscription": "Subscrição", "paymentDetails": "Detalhes de pagamento", - "manageFamily": "Gerenciar família", - "contactToManageSubscription": "Entre em contato conosco em support@ente.io para gerenciar sua assinatura {provider}.", - "renewSubscription": "Renovar assinatura", - "cancelSubscription": "Cancelar assinatura", - "areYouSureYouWantToRenew": "Deseja renovar?", - "yesRenew": "Sim", - "areYouSureYouWantToCancel": "Deseja cancelar?", - "yesCancel": "Sim", + "manageFamily": "Gerir família", + "contactToManageSubscription": "Contacte-nos em support@ente.io para gerir a sua subscrição {provider}", + "renewSubscription": "Renovar subscrição", + "cancelSubscription": "Cancelar subscrição", + "areYouSureYouWantToRenew": "Tem a certeza de que pretende renovar?", + "yesRenew": "Sim, Renovar", + "areYouSureYouWantToCancel": "Tem a certeza de que quer cancelar?", + "yesCancel": "Sim, cancelar", "failedToRenew": "Falhou ao renovar", "failedToCancel": "Falhou ao cancelar", "twoMonthsFreeOnYearlyPlans": "2 meses grátis em planos anuais", @@ -636,10 +614,10 @@ "description": "The text to display for yearly plans", "type": "text" }, - "confirmPlanChange": "Confirmar mudança de plano", - "areYouSureYouWantToChangeYourPlan": "Deseja trocar de plano?", - "youCannotDowngradeToThisPlan": "Você não pode rebaixar para este plano", - "cancelOtherSubscription": "Primeiramente cancele sua assinatura existente do {paymentProvider}", + "confirmPlanChange": "Confirmar alteração de plano", + "areYouSureYouWantToChangeYourPlan": "Tem a certeza de que pretende alterar o seu plano?", + "youCannotDowngradeToThisPlan": "Não é possível fazer o downgrade para este plano", + "cancelOtherSubscription": "Por favor, cancele primeiro a sua subscrição existente de {paymentProvider}", "@cancelOtherSubscription": { "description": "The text to display when the user has an existing subscription from a different payment provider", "type": "text", @@ -650,24 +628,24 @@ } } }, - "optionalAsShortAsYouLike": "Opcional, tão curto como quiser...", + "optionalAsShortAsYouLike": "Opcional, o mais breve que quiser...", "send": "Enviar", - "askCancelReason": "Sua assinatura foi cancelada. Deseja compartilhar o motivo?", - "thankYouForSubscribing": "Obrigado por assinar!", - "yourPurchaseWasSuccessful": "Sua compra foi efetuada com sucesso", - "yourPlanWasSuccessfullyUpgraded": "Seu plano foi atualizado com sucesso", - "yourPlanWasSuccessfullyDowngraded": "Seu plano foi rebaixado com sucesso", - "yourSubscriptionWasUpdatedSuccessfully": "Sua assinatura foi atualizada com sucesso", + "askCancelReason": "A sua subscrição foi cancelada. Gostaria de partilhar o motivo?", + "thankYouForSubscribing": "Obrigado pela sua subscrição!", + "yourPurchaseWasSuccessful": "Sua compra foi realizada com sucesso", + "yourPlanWasSuccessfullyUpgraded": "O seu plano foi atualizado com sucesso", + "yourPlanWasSuccessfullyDowngraded": "O seu plano foi rebaixado com sucesso", + "yourSubscriptionWasUpdatedSuccessfully": "A sua subscrição foi actualizada com sucesso", "googlePlayId": "ID do Google Play", "appleId": "ID da Apple", - "playstoreSubscription": "Assinatura da PlayStore", - "appstoreSubscription": "Assinatura da AppStore", - "subAlreadyLinkedErrMessage": "Seu {id} já está vinculado a outra conta Ente. Se você gostaria de usar seu {id} com esta conta, entre em contato conosco\"", - "visitWebToManage": "Visite o web.ente.io para gerenciar sua assinatura", - "couldNotUpdateSubscription": "Não foi possível atualizar a assinatura", - "pleaseContactSupportAndWeWillBeHappyToHelp": "Entre em contato com support@ente.io e nós ficaremos felizes em ajudar!", + "playstoreSubscription": "Subscrição da PlayStore", + "appstoreSubscription": "Subscrição da AppStore", + "subAlreadyLinkedErrMessage": "Seu {id} já está vinculado a outra conta Ente.\nSe você gostaria de usar seu {id} com esta conta, por favor contate nosso suporte''", + "visitWebToManage": "Visite web.ente.io para gerir a sua subscrição", + "couldNotUpdateSubscription": "Não foi possível atualizar a subscrição", + "pleaseContactSupportAndWeWillBeHappyToHelp": "Por favor, entre em contato com support@ente.io e nós ficaremos felizes em ajudar!", "paymentFailed": "O pagamento falhou", - "paymentFailedTalkToProvider": "Fale com o suporte {providerName} se você foi cobrado", + "paymentFailedTalkToProvider": "Por favor, fale com o suporte {providerName} se você foi cobrado", "@paymentFailedTalkToProvider": { "description": "The text to display when the payment failed", "type": "text", @@ -678,41 +656,39 @@ } } }, - "continueOnFreeTrial": "Continuar com a avaliação grátis", - "areYouSureYouWantToExit": "Tem certeza de que queira sair?", + "continueOnFreeTrial": "Continuar em teste gratuito", + "areYouSureYouWantToExit": "Tem certeza de que deseja sair?", "thankYou": "Obrigado", - "failedToVerifyPaymentStatus": "Falhou ao verificar estado do pagamento", - "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde mais algum tempo antes de tentar novamente", - "paymentFailedMessage": "Infelizmente o pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!", + "failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento", + "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente", + "paymentFailedMessage": "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!", "youAreOnAFamilyPlan": "Você está em um plano familiar!", - "contactFamilyAdmin": "Entre em contato com {familyAdminEmail} para gerenciar sua assinatura", - "leaveFamily": "Sair do plano familiar", - "areYouSureThatYouWantToLeaveTheFamily": "Você tem certeza que queira sair do plano familiar?", + "contactFamilyAdmin": "Contacte {familyAdminEmail} para gerir a sua subscrição", + "leaveFamily": "Deixar plano famíliar", + "areYouSureThatYouWantToLeaveTheFamily": "Tem certeza que deseja sair do plano familiar?", "leave": "Sair", - "rateTheApp": "Avalie o aplicativo", + "rateTheApp": "Avaliar aplicação", "startBackup": "Iniciar cópia de segurança", - "noPhotosAreBeingBackedUpRightNow": "No momento não há fotos sendo copiadas com segurança", + "noPhotosAreBeingBackedUpRightNow": "No momento não há backup de fotos sendo feito", "preserveMore": "Preservar mais", - "grantFullAccessPrompt": "Permita o acesso a todas as fotos nas opções do aplicativo", - "allowPermTitle": "Permita acesso às Fotos", - "allowPermBody": "Permita o acesso a suas fotos das Configurações para que Ente possa exibir e copiar com segurança sua biblioteca.", - "openSettings": "Abrir opções", + "grantFullAccessPrompt": "Por favor, permita o acesso a todas as fotos nas definições do aplicativo", + "openSettings": "Abrir Definições", "selectMorePhotos": "Selecionar mais fotos", - "existingUser": "Usuário existente", - "privateBackups": "Cópias privadas", + "existingUser": "Utilizador existente", + "privateBackups": "Backups privados", "forYourMemories": "para suas memórias", - "endtoendEncryptedByDefault": "Criptografado de ponta a ponta por padrão", + "endtoendEncryptedByDefault": "Criptografia de ponta a ponta por padrão", "safelyStored": "Armazenado com segurança", "atAFalloutShelter": "em um abrigo avançado", "designedToOutlive": "Feito para ter longevidade", "available": "Disponível", - "everywhere": "em todas as partes", - "androidIosWebDesktop": "Android, iOS, Web, Computador", - "mobileWebDesktop": "Celular, Web, Computador", + "everywhere": "em todo o lado", + "androidIosWebDesktop": "Android, iOS, Web, Desktop", + "mobileWebDesktop": "Mobile, Web, Desktop", "newToEnte": "Novo no Ente", - "pleaseLoginAgain": "Registre-se novamente", - "autoLogoutMessage": "Devido ao ocorrido de erros técnicos, você foi desconectado. Pedimos desculpas pela inconveniência.", - "yourSubscriptionHasExpired": "A sua assinatura expirou", + "pleaseLoginAgain": "Por favor, inicie sessão novamente", + "autoLogoutMessage": "Devido a uma falha técnica, a sua sessão foi encerrada. Pedimos desculpas pelo incómodo.", + "yourSubscriptionHasExpired": "A sua subscrição expirou", "storageLimitExceeded": "Limite de armazenamento excedido", "upgrade": "Atualizar", "raiseTicket": "Abrir ticket", @@ -720,19 +696,19 @@ "description": "Button text for raising a support tickets in case of unhandled errors during backup", "type": "text" }, - "backupFailed": "Falhou ao copiar com segurança", - "couldNotBackUpTryLater": "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde.", + "backupFailed": "Backup falhou", + "couldNotBackUpTryLater": "Não foi possível fazer o backup de seus dados.\nTentaremos novamente mais tarde.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles", "pleaseGrantPermissions": "Por favor, conceda as permissões", - "grantPermission": "Conceder permissões", - "privateSharing": "Compartilhamento privado", - "shareOnlyWithThePeopleYouWant": "Compartilhar apenas com as pessoas que você quiser", + "grantPermission": "Conceder permissão", + "privateSharing": "Partilha privada", + "shareOnlyWithThePeopleYouWant": "Partilhar apenas com as pessoas que deseja", "usePublicLinksForPeopleNotOnEnte": "Usar links públicos para pessoas que não estão no Ente", - "allowPeopleToAddPhotos": "Permitir que pessoas adicionem fotos", - "shareAnAlbumNow": "Compartilhar um álbum agora", - "collectEventPhotos": "Coletar fotos de evento", + "allowPeopleToAddPhotos": "Permitir que as pessoas adicionem fotos", + "shareAnAlbumNow": "Partilhar um álbum", + "collectEventPhotos": "Coletar fotos do evento", "sessionExpired": "Sessão expirada", - "loggingOut": "Desconectando...", + "loggingOut": "Terminar a sessão...", "@onDevice": { "description": "The text displayed above folders/albums stored on device", "type": "text" @@ -742,14 +718,14 @@ "description": "The text displayed above albums backed up to Ente", "type": "text" }, - "onEnte": "No ente", + "onEnte": "Em ente", "name": "Nome", - "newest": "Mais recente", + "newest": "Recentes", "lastUpdated": "Última atualização", - "deleteEmptyAlbums": "Excluir álbuns vazios", - "deleteEmptyAlbumsWithQuestionMark": "Excluir álbuns vazios?", - "deleteAlbumsDialogBody": "Isso excluirá todos os álbuns vazios. Isso é útil quando você quiser reduzir a desordem no seu álbum.", - "deleteProgress": "Excluindo {currentlyDeleting} / {totalCount}", + "deleteEmptyAlbums": "Apagar álbuns vazios", + "deleteEmptyAlbumsWithQuestionMark": "Apagar álbuns vazios?", + "deleteAlbumsDialogBody": "Esta ação elimina todos os álbuns vazios. Isto é útil quando pretende reduzir a confusão na sua lista de álbuns.", + "deleteProgress": "Apagar {currentlyDeleting} / {totalCount}", "genericProgress": "Processando {currentlyProcessing} / {totalCount}", "@genericProgress": { "description": "Generic progress text to display when processing multiple items", @@ -765,66 +741,58 @@ } } }, - "permanentlyDelete": "Excluir permanentemente", - "canOnlyCreateLinkForFilesOwnedByYou": "Só é possível criar um link para arquivos pertencentes a você", + "permanentlyDelete": "Eliminar permanentemente", + "canOnlyCreateLinkForFilesOwnedByYou": "Só pode criar um link para arquivos pertencentes a você", "publicLinkCreated": "Link público criado", - "youCanManageYourLinksInTheShareTab": "Você pode gerenciar seus links na aba de compartilhamento.", + "youCanManageYourLinksInTheShareTab": "Pode gerir as suas ligações no separador partilhar.", "linkCopiedToClipboard": "Link copiado para a área de transferência", "restore": "Restaurar", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", "type": "text" }, - "moveToAlbum": "Mover para o álbum", - "unhide": "Desocultar", + "moveToAlbum": "Mover para álbum", + "unhide": "Mostrar", "unarchive": "Desarquivar", "favorite": "Favorito", - "removeFromFavorite": "Desfavoritar", - "shareLink": "Compartilhar link", - "createCollage": "Criar colagem", - "saveCollage": "Salvar colagem", - "collageSaved": "Colagem salva na galeria", + "removeFromFavorite": "Remover dos favoritos", + "shareLink": "Partilhar link", + "createCollage": "Criar coleção", + "saveCollage": "Guardar colagem", + "collageSaved": "Colagem guardada na galeria", "collageLayout": "Layout", "addToEnte": "Adicionar ao Ente", "addToAlbum": "Adicionar ao álbum", - "delete": "Excluir", + "delete": "Apagar", "hide": "Ocultar", - "share": "Compartilhar", - "unhideToAlbum": "Desocultar para o álbum", + "share": "Partilhar", + "unhideToAlbum": "Mostrar para o álbum", "restoreToAlbum": "Restaurar para álbum", - "moveItem": "{count, plural, one {Mover item} other {Mover itens}}", - "@moveItem": { - "description": "Page title while moving one or more items to an album" - }, - "addItem": "{count, plural, one {Adicionar item} other {Adicionar itens}}", - "@addItem": { - "description": "Page title while adding one or more items to album" - }, "createOrSelectAlbum": "Criar ou selecionar álbum", "selectAlbum": "Selecionar álbum", "searchByAlbumNameHint": "Nome do álbum", "albumTitle": "Título do álbum", - "enterAlbumName": "Inserir nome do álbum", - "restoringFiles": "Restaurando arquivos...", - "movingFilesToAlbum": "Movendo arquivos para o álbum...", - "unhidingFilesToAlbum": "Desocultando arquivos para o álbum", - "canNotUploadToAlbumsOwnedByOthers": "Não é possível enviar para álbuns pertencentes a outros", - "uploadingFilesToAlbum": "Enviando arquivos para o álbum...", - "addedSuccessfullyTo": "Adicionado com sucesso a {albumName}", + "enterAlbumName": "Introduzir nome do álbum", + "restoringFiles": "Restaurar arquivos...", + "movingFilesToAlbum": "Mover arquivos para o álbum...", + "unhidingFilesToAlbum": "Desocultar ficheiros para o álbum", + "canNotUploadToAlbumsOwnedByOthers": "Não é possível fazer upload para álbuns pertencentes a outros", + "uploadingFilesToAlbum": "Enviar ficheiros para o álbum...", + "addedSuccessfullyTo": "Adicionado com sucesso a {albumName}", "movedSuccessfullyTo": "Movido com sucesso para {albumName}", "thisAlbumAlreadyHDACollaborativeLink": "Este álbum já tem um link colaborativo", "collaborativeLinkCreatedFor": "Link colaborativo criado para {albumName}", - "askYourLovedOnesToShare": "Peça que seus entes queridos compartilhem", + "askYourLovedOnesToShare": "Peça aos seus entes queridos para partilharem", "invite": "Convidar", - "shareYourFirstAlbum": "Compartilhar seu primeiro álbum", - "sharedWith": "Compartilhado com {emailIDs}", - "sharedWithMe": "Compartilhado comigo", - "sharedByMe": "Compartilhada por mim", - "doubleYourStorage": "Duplique seu armazenamento", - "referFriendsAnd2xYourPlan": "Recomende seus amigos e duplique seu plano", - "shareAlbumHint": "Abra um álbum e toque no botão compartilhar no canto superior direito para compartilhar.", - "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Os itens exibem o número de dias restantes antes da exclusão permanente", - "trashDaysLeft": "{count, plural, =0 {Em breve} =1 {1 dia} other {{count} dias}}", + "shareYourFirstAlbum": "Partilhe o seu primeiro álbum", + "sharedWith": "Partilhado com {emailIDs}", + "sharedWithMe": "Partilhado comigo", + "sharedByMe": "Partilhado por mim", + "doubleYourStorage": "Duplicar o seu armazenamento", + "referFriendsAnd2xYourPlan": "Recomende amigos e duplique o seu plano", + "shareAlbumHint": "Abra um álbum e toque no botão de partilha no canto superior direito para partilhar", + "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Os itens mostram o número de dias restantes antes da eliminação permanente", + "trashDaysLeft": "{count, plural, =0 {Brevemente} =1 {1 dia} other {{count} dias}}", "@trashDaysLeft": { "description": "Text to indicate number of days remaining before permanent deletion", "placeholders": { @@ -834,7 +802,7 @@ } } }, - "deleteAll": "Excluir tudo", + "deleteAll": "Apagar tudo", "renameAlbum": "Renomear álbum", "convertToAlbum": "Converter para álbum", "setCover": "Definir capa", @@ -842,63 +810,62 @@ "description": "Text to set cover photo for an album" }, "sortAlbumsBy": "Ordenar por", - "sortNewestFirst": "Recentes primeiro", - "sortOldestFirst": "Antigos primeiro", + "sortNewestFirst": "Mais recentes primeiro", + "sortOldestFirst": "Mais antigos primeiro", "rename": "Renomear", "leaveSharedAlbum": "Sair do álbum compartilhado?", "leaveAlbum": "Sair do álbum", - "photosAddedByYouWillBeRemovedFromTheAlbum": "Suas fotos adicionadas serão removidas do álbum", - "youveNoFilesInThisAlbumThatCanBeDeleted": "Você não tem arquivos neste álbum que possam ser excluídos", - "youDontHaveAnyArchivedItems": "Você não tem nenhum item arquivado.", - "ignoredFolderUploadReason": "Alguns arquivos neste álbum são ignorados do envio porque eles foram anteriormente excluídos do Ente.", - "resetIgnoredFiles": "Redefinir arquivos ignorados", - "deviceFilesAutoUploading": "Arquivos adicionados ao álbum do dispositivo serão automaticamente enviados para o Ente.", - "turnOnBackupForAutoUpload": "Ative a cópia de segurança para automaticamente enviar arquivos adicionados à pasta do dispositivo para o Ente.", + "photosAddedByYouWillBeRemovedFromTheAlbum": "As fotos adicionadas por si serão removidas do álbum", + "youveNoFilesInThisAlbumThatCanBeDeleted": "Não existem ficheiros neste álbum que possam ser eliminados", + "youDontHaveAnyArchivedItems": "Não tem nenhum item arquivado.", + "ignoredFolderUploadReason": "Alguns ficheiros deste álbum não podem ser carregados porque foram anteriormente eliminados do Ente.", + "resetIgnoredFiles": "Repor ficheiros ignorados", + "deviceFilesAutoUploading": "Os ficheiros adicionados a este álbum de dispositivo serão automaticamente transferidos para o Ente.", + "turnOnBackupForAutoUpload": "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o Ente.", "noHiddenPhotosOrVideos": "Sem fotos ou vídeos ocultos", - "toHideAPhotoOrVideo": "Para ocultar uma foto ou vídeo", - "openTheItem": "• Abra a foto ou vídeo", + "toHideAPhotoOrVideo": "Para ocultar uma foto ou um vídeo", + "openTheItem": "• Abra o item", "clickOnTheOverflowMenu": "• Clique no menu adicional", - "click": "• Clique", + "click": "Clique", "nothingToSeeHere": "Nada para ver aqui! 👀", "unarchiveAlbum": "Desarquivar álbum", "archiveAlbum": "Arquivar álbum", - "calculating": "Calculando...", - "pleaseWaitDeletingAlbum": "Aguarde, excluindo álbum", - "searchByExamples": "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (ex.: \"2022\", \"Janeiro\")\n• Temporadas (ex.: \"Natal\")\n• Tags (ex.: \"#divertido\")", - "youCanTrySearchingForADifferentQuery": "Você pode tentar buscar por outra consulta.", - "noResultsFound": "Nenhum resultado encontrado", + "calculating": "Calcular...", + "pleaseWaitDeletingAlbum": "Por favor aguarde, apagar o álbum", + "searchByExamples": "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (e.. \"2022\", \"Janeiro\")\n• Feriados (por exemplo, \"Natal\")\n• Descrições de fotos (por exemplo, \"#divertido\")", + "youCanTrySearchingForADifferentQuery": "Pode tentar pesquisar uma consulta diferente.", + "noResultsFound": "Não foram encontrados resultados", "addedBy": "Adicionado por {emailOrName}", "loadingExifData": "Carregando dados EXIF...", "viewAllExifData": "Ver todos os dados EXIF", "noExifData": "Sem dados EXIF", - "thisImageHasNoExifData": "Esta imagem não possui dados EXIF", + "thisImageHasNoExifData": "Esta imagem não tem dados exif", "exif": "EXIF", "noResults": "Nenhum resultado", - "weDontSupportEditingPhotosAndAlbumsThatYouDont": "Não suportamos a edição de fotos e álbuns que você ainda não possui", - "failedToFetchOriginalForEdit": "Falhou ao obter original para edição", + "weDontSupportEditingPhotosAndAlbumsThatYouDont": "Não suportamos a edição de fotos e álbuns que ainda não possui", + "failedToFetchOriginalForEdit": "Falha ao obter original para edição", "close": "Fechar", "setAs": "Definir como", - "fileSavedToGallery": "Arquivo salvo na galeria", - "filesSavedToGallery": "Arquivos salvos na galeria", - "fileFailedToSaveToGallery": "Falhou ao salvar arquivo na galeria", - "download": "Baixar", + "fileSavedToGallery": "Arquivo guardado na galeria", + "filesSavedToGallery": "Arquivos guardados na galeria", + "fileFailedToSaveToGallery": "Falha ao guardar o ficheiro na galeria", + "download": "Download", "pressAndHoldToPlayVideo": "Pressione e segure para reproduzir o vídeo", "pressAndHoldToPlayVideoDetailed": "Pressione e segure na imagem para reproduzir o vídeo", - "downloadFailed": "Falhou ao baixar", + "downloadFailed": "Falha no download", "deduplicateFiles": "Arquivos duplicados", - "deselectAll": "Deselecionar tudo", - "reviewDeduplicateItems": "Reveja e exclua os itens que você acredita serem duplicados.", + "deselectAll": "Desmarcar tudo", + "reviewDeduplicateItems": "Reveja e elimine os itens que considera serem duplicados.", "clubByCaptureTime": "Agrupar por tempo de captura", - "clubByFileName": "Agrupar por nome do arquivo", + "clubByFileName": "Agrupar pelo nome de arquivo", "count": "Contagem", "totalSize": "Tamanho total", - "longpressOnAnItemToViewInFullscreen": "Mantenha pressionado em um item para visualizá-lo em tela cheia", + "longpressOnAnItemToViewInFullscreen": "Pressione e segure em um item para ver em tela cheia", "decryptingVideo": "Descriptografando vídeo...", - "authToViewYourMemories": "Autentique-se para ver suas memórias", + "authToViewYourMemories": "Por favor, autentique-se para ver suas memórias", "unlock": "Desbloquear", - "freeUpSpace": "Liberar espaço", - "freeUpSpaceSaving": "{count, plural, one {Ele pode ser excluído do dispositivo para liberar {formattedSize}} other {Eles podem ser excluídos do dispositivo para liberar {formattedSize}}}", - "filesBackedUpInAlbum": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} deste álbum foi copiado com segurança", + "freeUpSpace": "Libertar espaço", + "filesBackedUpInAlbum": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} neste álbum teve um backup seguro", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -913,7 +880,7 @@ } } }, - "filesBackedUpFromDevice": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} deste dispositivo foi copiado com segurança", + "filesBackedUpFromDevice": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} neste dispositivo teve um backup seguro", "@filesBackedUpFromDevice": { "description": "Text to tell user how many files have been backed up from this device", "placeholders": { @@ -928,43 +895,31 @@ } } }, - "@freeUpSpaceSaving": { - "description": "Text to tell user how much space they can free up by deleting items from the device" - }, - "freeUpAccessPostDelete": "Você ainda pode acessá-{count, plural, one {lo} other {los}} no Ente, contanto que você tenha uma assinatura ativa", - "@freeUpAccessPostDelete": { - "placeholders": { - "count": { - "example": "1", - "type": "int" - } - } - }, - "freeUpAmount": "Liberar {sizeInMBorGB}", - "thisEmailIsAlreadyInUse": "Este e-mail já está sendo usado", - "incorrectCode": "Código incorreto", - "authenticationFailedPleaseTryAgain": "Falha na autenticação. Tente novamente", - "verificationFailedPleaseTryAgain": "Falha na verificação. Tente novamente", - "authenticating": "Autenticando...", - "authenticationSuccessful": "Autenticado com sucesso!", + "freeUpAmount": "Libertar {sizeInMBorGB}", + "thisEmailIsAlreadyInUse": "Este email já está em uso", + "incorrectCode": "Código incorrecto", + "authenticationFailedPleaseTryAgain": "Falha na autenticação, por favor tente novamente", + "verificationFailedPleaseTryAgain": "Falha na verificação, por favor tente novamente", + "authenticating": "A Autenticar...", + "authenticationSuccessful": "Autenticação bem sucedida!", "incorrectRecoveryKey": "Chave de recuperação incorreta", "theRecoveryKeyYouEnteredIsIncorrect": "A chave de recuperação inserida está incorreta", - "twofactorAuthenticationSuccessfullyReset": "Autenticação de dois fatores redefinida com sucesso", - "pleaseVerifyTheCodeYouHaveEntered": "Verifique o código inserido", + "twofactorAuthenticationSuccessfullyReset": "Autenticação de dois fatores redefinida com êxito", + "pleaseVerifyTheCodeYouHaveEntered": "Por favor, verifique se o código que você inseriu", "pleaseContactSupportIfTheProblemPersists": "Por favor, contate o suporte se o problema persistir", "twofactorAuthenticationHasBeenDisabled": "A autenticação de dois fatores foi desativada", - "sorryTheCodeYouveEnteredIsIncorrect": "O código inserido está incorreto", - "yourVerificationCodeHasExpired": "O código de verificação expirou", - "emailChangedTo": "E-mail alterado para {newEmail}", - "verifying": "Verificando...", - "disablingTwofactorAuthentication": "Desativando a autenticação de dois fatores...", + "sorryTheCodeYouveEnteredIsIncorrect": "Desculpe, o código inserido está incorreto", + "yourVerificationCodeHasExpired": "O seu código de verificação expirou", + "emailChangedTo": "Email alterado para {newEmail}", + "verifying": "A verificar…", + "disablingTwofactorAuthentication": "Desativar a autenticação de dois factores...", "allMemoriesPreserved": "Todas as memórias preservadas", "loadingGallery": "Carregando galeria...", "syncing": "Sincronizando...", - "encryptingBackup": "Criptografando cópia de segurança...", + "encryptingBackup": "Criptografando backup...", "syncStopped": "Sincronização interrompida", "syncProgress": "{completed}/{total} memórias preservadas", - "uploadingMultipleMemories": "Preservando {count} memórias...", + "uploadingMultipleMemories": "Preservar {count} memórias...", "@uploadingMultipleMemories": { "description": "Text to tell user how many memories are being preserved", "placeholders": { @@ -973,7 +928,7 @@ } } }, - "uploadingSingleMemory": "Preservando 1 memória...", + "uploadingSingleMemory": "Preservar 1 memória...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -985,100 +940,99 @@ } } }, - "archiving": "Arquivando...", - "unarchiving": "Desarquivando...", + "archiving": "Arquivar...", + "unarchiving": "Desarquivar...", "successfullyArchived": "Arquivado com sucesso", "successfullyUnarchived": "Desarquivado com sucesso", "renameFile": "Renomear arquivo", "enterFileName": "Inserir nome do arquivo", - "filesDeleted": "Arquivos excluídos", + "filesDeleted": "Arquivos apagados", "selectedFilesAreNotOnEnte": "Os arquivos selecionados não estão no Ente", "thisActionCannotBeUndone": "Esta ação não pode ser desfeita", - "emptyTrash": "Esvaziar a lixeira?", - "permDeleteWarning": "Todos os itens na lixeira serão excluídos permanentemente\n\nEsta ação não pode ser desfeita", + "emptyTrash": "Esvaziar lixo?", + "permDeleteWarning": "Todos os itens no lixo serão permanentemente eliminados\n\n\nEsta ação não pode ser anulada", "empty": "Esvaziar", - "couldNotFreeUpSpace": "Não foi possível liberar espaço", - "permanentlyDeleteFromDevice": "Excluir permanentemente do dispositivo?", - "someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos arquivos que você está tentando excluir só estão disponíveis no seu dispositivo e não podem ser recuperados se forem excluídos", - "theyWillBeDeletedFromAllAlbums": "Eles serão excluídos de todos os álbuns.", - "someItemsAreInBothEnteAndYourDevice": "Alguns itens estão em ambos o Ente quanto no seu dispositivo.", - "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira.", - "theseItemsWillBeDeletedFromYourDevice": "Estes itens serão excluídos do seu dispositivo.", - "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe.", + "couldNotFreeUpSpace": "Não foi possível libertar espaço", + "permanentlyDeleteFromDevice": "Apagar permanentemente do dispositivo?", + "someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos ficheiros que está a tentar eliminar só estão disponíveis no seu dispositivo e não podem ser recuperados se forem eliminados", + "theyWillBeDeletedFromAllAlbums": "Serão eliminados de todos os álbuns.", + "someItemsAreInBothEnteAndYourDevice": "Alguns itens estão tanto no Ente como no seu dispositivo.", + "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão eliminados de todos os álbuns e movidos para o lixo.", + "theseItemsWillBeDeletedFromYourDevice": "Estes itens serão eliminados do seu dispositivo.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo correu mal. Por favor, tente novamente após algum tempo. Se o erro persistir, contacte a nossa equipa de apoio.", "error": "Erro", - "tempErrorContactSupportIfPersists": "Parece que algo deu errado. Tente novamente mais tarde. Caso o erro persistir, por favor, entre em contato com nossa equipe.", - "networkHostLookUpErr": "Não foi possível conectar-se ao Ente, verifique suas configurações de rede e entre em contato com o suporte se o erro persistir.", - "networkConnectionRefusedErr": "Não foi possível conectar ao Ente, tente novamente mais tarde. Se o erro persistir, entre em contato com o suporte.", - "cachedData": "Dados armazenados em cache", + "tempErrorContactSupportIfPersists": "Parece que algo correu mal. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contacto com a nossa equipa de suporte.", + "networkHostLookUpErr": "Não foi possível estabelecer ligação ao Ente. Verifique as definições de rede e contacte o serviço de apoio se o erro persistir.", + "networkConnectionRefusedErr": "Não foi possível conectar ao Ente, tente novamente após algum tempo. Se o erro persistir, entre em contato com o suporte.", + "cachedData": "Dados em cache", "clearCaches": "Limpar cache", "remoteImages": "Imagens remotas", "remoteVideos": "Vídeos remotos", "remoteThumbnails": "Miniaturas remotas", "pendingSync": "Sincronização pendente", "localGallery": "Galeria local", - "todaysLogs": "Registros de hoje", - "viewLogs": "Ver registros", - "logsDialogBody": "Isso enviará através dos registros para ajudar-nos a resolver seu problema. Saiba que, nome de arquivos serão incluídos para ajudar a buscar problemas com arquivos específicos.", - "preparingLogs": "Preparando registros...", - "emailYourLogs": "Enviar registros por e-mail", - "pleaseSendTheLogsTo": "Envie os registros para \n{toEmail}", - "copyEmailAddress": "Copiar endereço de e-mail", - "exportLogs": "Exportar registros", - "pleaseEmailUsAt": "Envie-nos um e-mail para {toEmail}", - "dismiss": "Descartar", + "todaysLogs": "Logs de hoje", + "viewLogs": "Ver logs", + "logsDialogBody": "Isto enviará os registos para nos ajudar a resolver o problema. Tenha em atenção que os nomes dos ficheiros serão incluídos para ajudar a localizar problemas com ficheiros específicos.", + "preparingLogs": "Preparando logs...", + "emailYourLogs": "Enviar logs por e-mail", + "pleaseSendTheLogsTo": "Por favor, envie os logs para \n{toEmail}", + "copyEmailAddress": "Copiar endereço de email", + "exportLogs": "Exportar logs", + "pleaseEmailUsAt": "Por favor, envie-nos um e-mail para {toEmail}", + "dismiss": "Rejeitar", "didYouKnow": "Você sabia?", - "loadingMessage": "Carregando suas fotos...", - "loadMessage1": "Você pode compartilhar sua assinatura com seus familiares", - "loadMessage2": "Nós preservamos mais de 30 milhões de memórias até então", + "loadingMessage": "Carregar as suas fotos...", + "loadMessage1": "Pode partilhar a sua subscrição com a sua família", "loadMessage3": "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo", "loadMessage4": "Todos os nossos aplicativos são de código aberto", "loadMessage5": "Nosso código-fonte e criptografia foram auditadas externamente", - "loadMessage6": "Você pode compartilhar links para seus álbuns com seus entes queridos", - "loadMessage7": "Nossos aplicativos móveis são executados em segundo plano para criptografar e copiar com segurança quaisquer fotos novas que você acessar", - "loadMessage8": "web.ente.io tem um enviador mais rápido", + "loadMessage6": "Deixar o álbum partilhado?", + "loadMessage7": "Nossos aplicativos móveis são executados em segundo plano para criptografar e fazer backup de quaisquer novas fotos que você clique", + "loadMessage8": "web.ente.io tem um envio mais rápido", "loadMessage9": "Nós usamos Xchacha20Poly1305 para criptografar seus dados com segurança", "photoDescriptions": "Descrições das fotos", "fileTypesAndNames": "Tipos de arquivo e nomes", "location": "Localização", "moments": "Momentos", - "searchFaceEmptySection": "As pessoas apareceram aqui quando a indexação for concluída", - "searchDatesEmptySection": "Buscar por data, mês ou ano", + "searchFaceEmptySection": "As pessoas serão mostradas aqui quando a indexação estiver concluída", + "searchDatesEmptySection": "Pesquisar por data, mês ou ano", "searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto", - "searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui", + "searchPeopleEmptySection": "Convide pessoas e verá todas as fotos partilhadas por elas aqui", "searchAlbumsEmptySection": "Álbuns", "searchFileTypesAndNamesEmptySection": "Tipos de arquivo e nomes", - "searchCaptionEmptySection": "Adicione marcações como \"#viagem\" nas informações das fotos para encontrá-las aqui com facilidade", + "searchCaptionEmptySection": "Adicione descrições como \"#trip\" nas informações das fotos para encontrá-las aqui rapidamente", "language": "Idioma", - "selectLanguage": "Selecionar idioma", + "selectLanguage": "Selecionar Idioma", "locationName": "Nome da localização", "addLocation": "Adicionar localização", "groupNearbyPhotos": "Agrupar fotos próximas", "kiloMeterUnit": "km", "addLocationButton": "Adicionar", "radius": "Raio", - "locationTagFeatureDescription": "Uma etiqueta de localização agrupa todas as fotos fotografadas em algum raio de uma foto", - "galleryMemoryLimitInfo": "Até 1.000 memórias exibidas na galeria", - "save": "Salvar", + "locationTagFeatureDescription": "Uma etiqueta de localização agrupa todas as fotos que foram tiradas num determinado raio de uma fotografia", + "galleryMemoryLimitInfo": "Até 1000 memórias mostradas na galeria", + "save": "Guardar", "centerPoint": "Ponto central", "pickCenterPoint": "Escolha o ponto central", - "useSelectedPhoto": "Usar foto selecionada", + "useSelectedPhoto": "Utilizar foto selecionada", "resetToDefault": "Redefinir para o padrão", "@resetToDefault": { "description": "Button text to reset cover photo to default" }, "edit": "Editar", - "deleteLocation": "Excluir localização", - "rotateLeft": "Girar para a esquerda", + "deleteLocation": "Apagar localização", + "rotateLeft": "Rodar para a esquerda", "flip": "Inverter", - "rotateRight": "Girar para a direita", - "saveCopy": "Salvar cópia", - "light": "Brilho", + "rotateRight": "Rodar para a direita", + "saveCopy": "Guardar cópia", + "light": "Claro", "color": "Cor", - "yesDiscardChanges": "Sim, descartar alterações", - "doYouWantToDiscardTheEditsYouHaveMade": "Você quer descartar as edições que você fez?", - "saving": "Salvando...", - "editsSaved": "Edições salvas", - "oopsCouldNotSaveEdits": "Opa! Não foi possível salvar as edições", + "yesDiscardChanges": "Sim, rejeitar alterações", + "doYouWantToDiscardTheEditsYouHaveMade": "Pretende eliminar as edições que efectuou?", + "saving": "A gravar...", + "editsSaved": "Edição guardada", + "oopsCouldNotSaveEdits": "Oops, não foi possível guardar as edições", "distanceInKMUnit": "km", "@distanceInKMUnit": { "description": "Unit for distance in km" @@ -1086,9 +1040,9 @@ "dayToday": "Hoje", "dayYesterday": "Ontem", "storage": "Armazenamento", - "usedSpace": "Espaço usado", + "usedSpace": "Espaço utilizado", "storageBreakupFamily": "Família", - "storageBreakupYou": "Você", + "storageBreakupYou": "Tu", "@storageBreakupYou": { "description": "Label to indicate how much storage you are using when you are part of a family plan" }, @@ -1096,10 +1050,10 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "availableStorageSpace": "{freeAmount} {storageUnit} livre", + "availableStorageSpace": "{freeAmount} {storageUnit} grátis", "appVersion": "Versão: {versionValue}", "verifyIDLabel": "Verificar", - "fileInfoAddDescHint": "Adicionar descrição...", + "fileInfoAddDescHint": "Acrescente uma descrição...", "editLocationTagTitle": "Editar localização", "setLabel": "Definir", "@setLabel": { @@ -1107,7 +1061,7 @@ }, "setRadius": "Definir raio", "familyPlanPortalTitle": "Família", - "familyPlanOverview": "Adicione 5 familiares para seu plano existente sem pagar nenhum custo adicional.\n\nCada membro ganha seu espaço privado, significando que eles não podem ver os arquivos dos outros a menos que eles sejam compartilhados.\n\nOs planos familiares estão disponíveis para clientes que já tem uma assinatura paga do Ente.\n\nAssine agora para iniciar!", + "familyPlanOverview": "Adicione 5 membros da família ao seu plano existente sem pagar mais.\n\n\nCada membro tem o seu próprio espaço privado e não pode ver os ficheiros dos outros, a menos que sejam partilhados.\n\n\nOs planos familiares estão disponíveis para clientes que tenham uma subscrição paga do Ente.\n\n\nSubscreva agora para começar!", "androidBiometricHint": "Verificar identidade", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -1128,31 +1082,31 @@ "@androidSignInTitle": { "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters." }, - "androidBiometricRequiredTitle": "Biométrica necessária", + "androidBiometricRequiredTitle": "Biometria necessária", "@androidBiometricRequiredTitle": { "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters." }, - "androidDeviceCredentialsRequiredTitle": "Credenciais necessários", + "androidDeviceCredentialsRequiredTitle": "Credenciais do dispositivo são necessárias", "@androidDeviceCredentialsRequiredTitle": { "description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters." }, - "androidDeviceCredentialsSetupDescription": "Credenciais necessários", + "androidDeviceCredentialsSetupDescription": "Credenciais do dispositivo necessárias", "@androidDeviceCredentialsSetupDescription": { "description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side." }, - "goToSettings": "Ir às opções", + "goToSettings": "Ir para as definições", "@goToSettings": { "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." }, - "androidGoToSettingsDescription": "A autenticação biométrica não está definida no dispositivo. Vá em 'Opções > Segurança' para adicionar a autenticação biométrica.", + "androidGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Vá a “Definições > Segurança” para adicionar a autenticação biométrica.", "@androidGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." }, - "iOSLockOut": "A autenticação biométrica está desativada. Bloqueie e desbloqueie sua tela para ativá-la.", + "iOSLockOut": "A autenticação biométrica está desativada. Por favor, bloqueie e desbloqueie o ecrã para ativá-la.", "@iOSLockOut": { "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." }, - "iOSGoToSettingsDescription": "A autenticação biométrica não está definida no dispositivo. Ative o Touch ID ou Face ID no dispositivo.", + "iOSGoToSettingsDescription": "A autenticação biométrica não está configurada no seu dispositivo. Active o Touch ID ou o Face ID no seu telemóvel.", "@iOSGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." }, @@ -1161,61 +1115,61 @@ "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, "openstreetmapContributors": "Contribuidores do OpenStreetMap", - "hostedAtOsmFrance": "Hospedado em OSM France", + "hostedAtOsmFrance": "Hospedado na OSM France", "map": "Mapa", "@map": { "description": "Label for the map view" }, "maps": "Mapas", "enableMaps": "Ativar mapas", - "enableMapsDesc": "Isso exibirá suas fotos em um mapa mundial.\n\nEste mapa é hospedado por Open Street Map, e as exatas localizações das fotos nunca serão compartilhadas.\n\nVocê pode desativar esta função a qualquer momento em Opções.", + "enableMapsDesc": "Esta opção mostra as suas fotografias num mapa do mundo.\n\n\nEste mapa é alojado pelo Open Street Map e as localizações exactas das suas fotografias nunca são partilhadas.\n\n\nPode desativar esta funcionalidade em qualquer altura nas Definições.", "quickLinks": "Links rápidos", "selectItemsToAdd": "Selecionar itens para adicionar", - "addSelected": "Adicionar selecionado", - "addFromDevice": "Adicionar do dispositivo", + "addSelected": "Adicionar selecionados", + "addFromDevice": "Adicionar a partir do dispositivo", "addPhotos": "Adicionar fotos", "noPhotosFoundHere": "Nenhuma foto encontrada aqui", - "zoomOutToSeePhotos": "Reduzir ampliação para ver as fotos", + "zoomOutToSeePhotos": "Diminuir o zoom para ver fotos", "noImagesWithLocation": "Nenhuma imagem com localização", "unpinAlbum": "Desafixar álbum", "pinAlbum": "Fixar álbum", "create": "Criar", "viewAll": "Ver tudo", - "nothingSharedWithYouYet": "Nada compartilhado com você ainda", - "noAlbumsSharedByYouYet": "Nenhum álbum compartilhado por você ainda", - "sharedWithYou": "Compartilhado com você", - "sharedByYou": "Compartilhado por você", - "inviteYourFriendsToEnte": "Convide seus amigos ao Ente", - "failedToDownloadVideo": "Falhou ao baixar vídeo", + "nothingSharedWithYouYet": "Ainda nada partilhado consigo", + "noAlbumsSharedByYouYet": "Ainda não há álbuns partilhados por si", + "sharedWithYou": "Partilhado consigo", + "sharedByYou": "Partilhado por si", + "inviteYourFriendsToEnte": "Convide seus amigos para o Ente", + "failedToDownloadVideo": "Falha ao fazer o download do vídeo", "hiding": "Ocultando...", "unhiding": "Reexibindo...", "successfullyHid": "Ocultado com sucesso", - "successfullyUnhid": "Desocultado com sucesso", - "crashReporting": "Relatório de erros", - "resumableUploads": "Envios retomáveis", - "addToHiddenAlbum": "Adicionar ao álbum oculto", - "moveToHiddenAlbum": "Mover ao álbum oculto", + "successfullyUnhid": "Reexibido com sucesso", + "crashReporting": "Relatório de falhas", + "resumableUploads": "Uploads reenviados", + "addToHiddenAlbum": "Adicionar a álbum oculto", + "moveToHiddenAlbum": "Mover para álbum oculto", "fileTypes": "Tipos de arquivo", - "deleteConfirmDialogBody": "Esta conta está vinculada aos outros aplicativos do Ente, se você usar algum. Seus dados baixados, entre todos os aplicativos do Ente, serão programados para exclusão, e sua conta será permanentemente excluída.", - "hearUsWhereTitle": "Como você soube do Ente? (opcional)", - "hearUsExplanation": "Não rastreamos instalações de aplicativo. Seria útil se você contasse onde nos encontrou!", - "viewAddOnButton": "Ver complementos", - "addOns": "Complementos", - "addOnPageSubtitle": "Detalhes dos complementos", + "deleteConfirmDialogBody": "Esta conta está ligada a outras aplicações Ente, se utilizar alguma. Os seus dados carregados, em todas as aplicações Ente, serão agendados para eliminação e a sua conta será permanentemente eliminada.", + "hearUsWhereTitle": "Como é que soube do Ente? (opcional)", + "hearUsExplanation": "Não monitorizamos as instalações de aplicações. Ajudaria se nos dissesse onde nos encontrou!", + "viewAddOnButton": "Ver addons", + "addOns": "addons", + "addOnPageSubtitle": "Detalhes dos addons", "yourMap": "Seu mapa", - "modifyYourQueryOrTrySearchingFor": "Altere o termo de busca ou tente consultar", + "modifyYourQueryOrTrySearchingFor": "Modifique a sua consulta ou tente pesquisar por", "blackFridaySale": "Promoção Black Friday", - "upto50OffUntil4thDec": "Com 50% de desconto, até 4 de dezembro", + "upto50OffUntil4thDec": "Até 50% de desconto, até 4 de dezembro.", "photos": "Fotos", "videos": "Vídeos", - "livePhotos": "Fotos animadas", - "searchHint1": "busca rápida no dispositivo", - "searchHint2": "Descrições e data das fotos", + "livePhotos": "Fotos Em Tempo Real", + "searchHint1": "Pesquisa rápida no dispositivo", + "searchHint2": "Datas das fotos, descrições", "searchHint3": "Álbuns, nomes de arquivos e tipos", - "searchHint4": "Localização", - "searchHint5": "Em breve: Busca mágica e rostos ✨", + "searchHint4": "Local", + "searchHint5": "Em breve: Rostos e pesquisa mágica ✨", "addYourPhotosNow": "Adicione suas fotos agora", - "searchResultCount": "{count, plural, one{{count} resultado encontrado} other{{count} resultados encontrados}}", + "searchResultCount": "{count, plural, one{{count} ano atrás} other{{count} anos atrás}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1232,88 +1186,86 @@ "@addNew": { "description": "Text to add a new item (location tag, album, caption etc)" }, - "contacts": "Contatos", - "noInternetConnection": "Sem conexão à internet", - "pleaseCheckYourInternetConnectionAndTryAgain": "Verifique sua conexão com a internet e tente novamente.", - "signOutFromOtherDevices": "Sair da conta em outros dispositivos", - "signOutOtherBody": "Se você acha que alguém possa saber da sua senha, você pode forçar desconectar sua conta de outros dispositivos.", - "signOutOtherDevices": "Sair em outros dispositivos", - "doNotSignOut": "Não sair", + "contacts": "Contactos", + "noInternetConnection": "Sem ligação à internet", + "pleaseCheckYourInternetConnectionAndTryAgain": "Por favor, verifique a sua ligação à Internet e tente novamente.", + "signOutFromOtherDevices": "Terminar sessão noutros dispositivos", + "signOutOtherBody": "Se pensa que alguém pode saber a sua palavra-passe, pode forçar todos os outros dispositivos que utilizam a sua conta a terminar a sessão.", + "signOutOtherDevices": "Terminar a sessão noutros dispositivos", + "doNotSignOut": "Não terminar a sessão", "editLocation": "Editar localização", - "selectALocation": "Selecionar localização", - "selectALocationFirst": "Primeiramente selecione uma localização", + "selectALocation": "Selecione uma localização", + "selectALocationFirst": "Selecione uma localização primeiro", "changeLocationOfSelectedItems": "Alterar a localização dos itens selecionados?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Edições à localização serão apenas vistos no Ente", - "cleanUncategorized": "Limpar não categorizado", - "cleanUncategorizedDescription": "Remover todos os arquivos não categorizados que estão presentes em outros álbuns", - "waitingForVerification": "Esperando verificação...", + "editsToLocationWillOnlyBeSeenWithinEnte": "Edições para localização só serão vistas dentro do Ente", + "cleanUncategorized": "Limpar sem categoria", + "cleanUncategorizedDescription": "Remover todos os arquivos da Não Categorizados que estão presentes em outros álbuns", + "waitingForVerification": "Aguardando verificação...", "passkey": "Chave de acesso", - "passkeyAuthTitle": "Verificação de chave de acesso", - "loginWithTOTP": "Registrar com TOTP", - "passKeyPendingVerification": "Verificação pendente", + "passkeyAuthTitle": "Verificação da chave de acesso", + "loginWithTOTP": "Iniciar sessão com TOTP", + "passKeyPendingVerification": "A verificação ainda está pendente", "loginSessionExpired": "Sessão expirada", - "loginSessionExpiredDetails": "Sua sessão expirou. Registre-se novamente.", + "loginSessionExpiredDetails": "A sua sessão expirou. Por favor, inicie sessão novamente.", "verifyPasskey": "Verificar chave de acesso", "playOnTv": "Reproduzir álbum na TV", - "pair": "Parear", + "pair": "Emparelhar", "deviceNotFound": "Dispositivo não encontrado", - "castInstruction": "Acesse cast.ente.io no dispositivo desejado para parear.\n\nInsira o código abaixo para reproduzir o álbum na sua TV.", - "deviceCodeHint": "Insira o código", - "joinDiscord": "Junte-se ao Discord", + "castInstruction": "Visite cast.ente.io no dispositivo que pretende emparelhar.\n\n\nIntroduza o código abaixo para reproduzir o álbum na sua TV.", + "deviceCodeHint": "Introduza o código", + "joinDiscord": "Juntar-se ao Discord", "locations": "Localizações", - "addAName": "Adicione um nome", - "findThemQuickly": "Busque-os rapidamente", + "addAName": "Adiciona um nome", + "findThemQuickly": "Ache-os rapidamente", "@findThemQuickly": { "description": "Subtitle to indicate that the user can find people quickly by name" }, - "findPeopleByName": "Busque pessoas facilmente pelo nome", - "addViewers": "{count, plural, one {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar visualizadores}}", - "addCollaborators": "{count, plural, one {Adicionar colaborador} one {Adicionar colaborador} other {Adicionar colaboradores}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Pressione um e-mail para verificar a criptografia ponta a ponta.", - "developerSettingsWarning": "Deseja modificar as Opções de Desenvolvedor?", - "developerSettings": "Opções de desenvolvedor", - "serverEndpoint": "Ponto final do servidor", - "invalidEndpoint": "Ponto final inválido", - "invalidEndpointMessage": "Desculpe, o ponto final inserido é inválido. Insira um ponto final válido e tente novamente.", - "endpointUpdatedMessage": "Ponto final atualizado com sucesso", - "customEndpoint": "Conectado à {endpoint}", + "findPeopleByName": "Encontrar pessoas rapidamente pelo nome", + "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.", + "developerSettingsWarning": "Tem a certeza de que pretende modificar as definições de programador?", + "developerSettings": "Definições do programador", + "serverEndpoint": "Endpoint do servidor", + "invalidEndpoint": "Endpoint inválido", + "invalidEndpointMessage": "Desculpe, o endpoint que introduziu é inválido. Introduza um ponto final válido e tente novamente.", + "endpointUpdatedMessage": "Endpoint atualizado com sucesso", + "customEndpoint": "Conectado a {endpoint}", "createCollaborativeLink": "Criar link colaborativo", - "search": "Buscar", + "search": "Pesquisar", "enterPersonName": "Inserir nome da pessoa", "enterName": "Inserir nome", - "savePerson": "Salvar pessoa", + "savePerson": "Guardar pessoa", "editPerson": "Editar pessoa", - "mergedPhotos": "Fotos mescladas", - "orMergeWithExistingPerson": "Ou mesclar com existente", + "mergedPhotos": "Fotos combinadas", + "orMergeWithExistingPerson": "Ou combinar com já existente", "enterDateOfBirth": "Aniversário (opcional)", "birthday": "Aniversário", "removePersonLabel": "Remover etiqueta da pessoa", - "autoPairDesc": "O pareamento automático só funciona com dispositivos que suportam o Chromecast.", - "manualPairDesc": "Parear com PIN funciona com qualquer tela que queira visualizar seu álbum.", - "connectToDevice": "Conectar ao dispositivo", - "autoCastDialogBody": "Você verá dispositivos de transmissão disponível aqui.", - "autoCastiOSPermission": "Certifique-se que as permissões da internet local estejam ligadas para o Ente Photos App, em opções.", + "autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", + "manualPairDesc": "Emparelhar com PIN funciona com qualquer ecrã onde pretenda ver o seu álbum.", + "connectToDevice": "Ligar ao dispositivo", + "autoCastDialogBody": "Verá os dispositivos Cast disponíveis aqui.", + "autoCastiOSPermission": "Certifique-se de que as permissões de Rede local estão activadas para a aplicação Ente Photos, nas Definições.", "noDeviceFound": "Nenhum dispositivo encontrado", "stopCastingTitle": "Parar transmissão", - "stopCastingBody": "Deseja parar a transmissão?", - "castIPMismatchTitle": "Falhou ao transmitir álbum", - "castIPMismatchBody": "Certifique-se de estar na mesma internet que a TV.", - "pairingComplete": "Pareamento concluído", - "savingEdits": "Salvando edições...", - "autoPair": "Pareamento automático", - "pairWithPin": "Parear com PIN", + "stopCastingBody": "Queres parar de fazer transmissão?", + "castIPMismatchTitle": "Falha ao transmitir álbum", + "castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.", + "pairingComplete": "Emparelhamento concluído", + "savingEdits": "Gravando edições...", + "autoPair": "Emparelhamento automático", + "pairWithPin": "Emparelhar com PIN", "faceRecognition": "Reconhecimento facial", "foundFaces": "Rostos encontrados", "clusteringProgress": "Progresso de agrupamento", - "indexingIsPaused": "A indexação parou, ela será retomada automaticamente quando o dispositivo estiver pronto.", - "trim": "Recortar", - "crop": "Cortar", - "rotate": "Girar", + "indexingIsPaused": "A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto.", + "trim": "Cortar", + "crop": "Recortar", + "rotate": "Rodar", "left": "Esquerda", "right": "Direita", "whatsNew": "O que há de novo", "reviewSuggestions": "Revisar sugestões", - "review": "Revisar", + "review": "Rever", "useAsCover": "Usar como capa", "notPersonLabel": "Não é {name}?", "@notPersonLabel": { @@ -1328,49 +1280,49 @@ "enable": "Ativar", "enabled": "Ativado", "moreDetails": "Mais detalhes", - "enableMLIndexingDesc": "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados", - "magicSearchHint": "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. 'flor', 'carro vermelho', 'identidade'", + "enableMLIndexingDesc": "O Ente suporta a aprendizagem automática no dispositivo para reconhecimento facial, pesquisa mágica e outras funcionalidades de pesquisa avançadas", + "magicSearchHint": "A pesquisa mágica permite pesquisar fotos por seu conteúdo, por exemplo, 'flor', 'carro vermelho', 'documentos de identidade'", "panorama": "Panorama", - "reenterPassword": "Reinserir senha", - "reenterPin": "Reinserir PIN", + "reenterPassword": "Insira novamente a palavra-passe", + "reenterPin": "Inserir PIN novamente", "deviceLock": "Bloqueio do dispositivo", "pinLock": "Bloqueio por PIN", - "next": "Próximo", - "setNewPassword": "Definir nova senha", - "enterPin": "Inserir PIN", - "setNewPin": "Definir PIN novo", - "appLock": "Bloqueio do aplicativo", - "noSystemLockFound": "Nenhum bloqueio do sistema encontrado", + "next": "Seguinte", + "setNewPassword": "Definir nova palavra-passe", + "enterPin": "Introduzir PIN", + "setNewPin": "Definir novo PIN", + "appLock": "Bloqueio de app", + "noSystemLockFound": "Nenhum bloqueio de sistema encontrado", "tapToUnlock": "Toque para desbloquear", "tooManyIncorrectAttempts": "Muitas tentativas incorretas", - "videoInfo": "Informações do vídeo", + "videoInfo": "Informação de Vídeo", "autoLock": "Bloqueio automático", "immediately": "Imediatamente", - "autoLockFeatureDescription": "Tempo após o qual o aplicativo bloqueia após ser colocado em segundo plano", + "autoLockFeatureDescription": "Tempo após o qual a aplicação bloqueia depois de ser colocada em segundo plano", "hideContent": "Ocultar conteúdo", - "hideContentDescriptionAndroid": "Oculta os conteúdos do aplicativo no seletor de aplicativos e desativa capturas de tela", - "hideContentDescriptionIos": "Oculta o conteúdo no seletor de aplicativos", - "passwordStrengthInfo": "A força da senha é calculada considerando o comprimento dos dígitos, carácteres usados, e se ou não a senha aparece nas 10.000 senhas usadas.", + "hideContentDescriptionAndroid": "Oculta o conteúdo da aplicação no alternador de aplicações e desactiva as capturas de ecrã", + "hideContentDescriptionIos": "Oculta o conteúdo da aplicação no alternador de aplicações", + "passwordStrengthInfo": "A força da palavra-passe é calculada tendo em conta o comprimento da palavra-passe, os caracteres utilizados e se a palavra-passe aparece ou não nas 10.000 palavras-passe mais utilizadas", "noQuickLinksSelected": "Nenhum link rápido selecionado", "pleaseSelectQuickLinksToRemove": "Selecione links rápidos para remover", "removePublicLinks": "Remover link público", "thisWillRemovePublicLinksOfAllSelectedQuickLinks": "Isto removerá links públicos de todos os links rápidos selecionados.", - "guestView": "Vista do convidado", - "guestViewEnablePreSteps": "Para ativar a vista do convidado, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema.", - "nameTheAlbum": "Nomear álbum", + "guestView": "Visão de convidado", + "guestViewEnablePreSteps": "Para ativar a vista de convidado, configure o código de acesso do dispositivo ou o bloqueio do ecrã nas definições do sistema.", + "nameTheAlbum": "Nomear o álbum", "collectPhotosDescription": "Crie um link onde seus amigos podem enviar fotos na qualidade original.", - "collect": "Coletar", - "appLockDescriptions": "Escolha entre a tela de bloqueio padrão do seu dispositivo e uma tela de bloqueio personalizada com PIN ou senha.", - "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para ativar o bloqueio do aplicativo, defina uma senha de acesso no dispositivo ou bloqueie sua tela nas opções do sistema.", - "authToViewPasskey": "Autentique-se para ver sua chave de acesso", - "loopVideoOn": "Repetir vídeo ativado", - "loopVideoOff": "Repetir vídeo desativado", - "localSyncErrorMessage": "Ocorreu um erro devido à sincronização de localização das fotos estar levando mais tempo que o esperado. Entre em contato conosco.", + "collect": "Recolher", + "appLockDescriptions": "Escolha entre o ecrã de bloqueio predefinido do seu dispositivo e um ecrã de bloqueio personalizado com um PIN ou uma palavra-passe.", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Para ativar o bloqueio de aplicações, configure o código de acesso do dispositivo ou o bloqueio de ecrã nas definições do sistema.", + "authToViewPasskey": "Autentique-se para ver a sua chave de acesso", + "loopVideoOn": "Repetir vídeo ligado", + "loopVideoOff": "Repetir vídeo desligado", + "localSyncErrorMessage": "Parece que algo correu mal, uma vez que a sincronização de fotografias locais está a demorar mais tempo do que o esperado. Contacte a nossa equipa de apoio", "showPerson": "Mostrar pessoa", "sort": "Ordenar", "mostRecent": "Mais recente", "mostRelevant": "Mais relevante", - "loadingYourPhotos": "Carregando suas fotos...", + "loadingYourPhotos": "Carregar as suas fotos...", "processingImport": "Processando {folderName}...", "personName": "Nome da pessoa", "addNewPerson": "Adicionar nova pessoa", @@ -1379,7 +1331,7 @@ "newPerson": "Nova pessoa", "addName": "Adicionar pessoa", "add": "Adicionar", - "extraPhotosFoundFor": "Fotos adicionais encontradas para {text}", + "extraPhotosFoundFor": "Fotos extras encontradas para {text}", "@extraPhotosFoundFor": { "placeholders": { "text": { @@ -1390,26 +1342,13 @@ "extraPhotosFound": "Fotos adicionais encontradas", "configuration": "Configuração", "localIndexing": "Indexação local", - "processed": "Processado", "resetPerson": "Remover", - "areYouSureYouWantToResetThisPerson": "Deseja redefinir esta pessoa?", - "allPersonGroupingWillReset": "Todos os agrupamentos dessa pessoa serão redefinidos, e você perderá todas as sugestões feitas por essa pessoa.", - "yesResetPerson": "Sim, redefinir pessoa", + "areYouSureYouWantToResetThisPerson": "Tens a certeza de que queres repor esta pessoa?", + "allPersonGroupingWillReset": "Todos os agrupamentos para esta pessoa serão reiniciados e perderá todas as sugestões feitas para esta pessoa", + "yesResetPerson": "Sim, repor pessoa", "onlyThem": "Apenas eles", - "checkingModels": "Verificando modelos...", - "enableMachineLearningBanner": "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial", - "searchDiscoverEmptySection": "As imagens serão exibidas aqui quando o processamento e sincronização for concluído", - "searchPersonsEmptySection": "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído", - "viewersSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 visualizadores} =1 {Adicionado 1 visualizador} other {Adicionado {count} visualizadores}}", - "@viewersSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of viewers that were successfully added to an album." - }, + "checkingModels": "A verificar modelos...", + "enableMachineLearningBanner": "Habilitar aprendizagem automática para pesquisa mágica e reconhecimento de rosto", "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 colaboradores} =1 {Adicionado 1 colaborador} other {Adicionado {count} colaboradores}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1420,18 +1359,18 @@ }, "description": "Number of collaborators that were successfully added to an album." }, - "accountIsAlreadyConfigured": "A conta já está configurada.", + "accountIsAlreadyConfigured": "A conta já está ajustada.", "sessionIdMismatch": "Incompatibilidade de ID de sessão", "@sessionIdMismatch": { "description": "In passkey page, deeplink is ignored because of session ID mismatch." }, - "failedToFetchActiveSessions": "Falhou ao obter sessões ativas", + "failedToFetchActiveSessions": "Falha ao obter sessões em atividade", "@failedToFetchActiveSessions": { "description": "In session page, warn user (in toast) that active sessions could not be fetched." }, - "failedToRefreshStripeSubscription": "Falhou ao atualizar assinatura", - "failedToPlayVideo": "Falhou ao reproduzir vídeo", - "uploadIsIgnoredDueToIgnorereason": "O envio é ignorado devido a {ignoreReason}", + "failedToRefreshStripeSubscription": "Falha ao atualizar subscrição", + "failedToPlayVideo": "Falha ao reproduzir multimédia", + "uploadIsIgnoredDueToIgnorereason": "Envio ignorado devido à {ignoreReason}", "@uploadIsIgnoredDueToIgnorereason": { "placeholders": { "ignoreReason": { @@ -1440,7 +1379,7 @@ } } }, - "typeOfGallerGallerytypeIsNotSupportedForRename": "O tipo de galeria {galleryType} não é suportado para renomear", + "typeOfGallerGallerytypeIsNotSupportedForRename": "Tipo de galeria {galleryType} não é permitido para renomear", "@typeOfGallerGallerytypeIsNotSupportedForRename": { "placeholders": { "galleryType": { @@ -1736,5 +1675,15 @@ "cLFamilyPlanDesc": "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar.", "cLBulkEdit": "Editar todas as datas", "cLBulkEditDesc": "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados.", - "curatedMemories": "Curated memories" + "curatedMemories": "Curated memories", + "onThisDay": "On this day", + "lookBackOnYourMemories": "Look back on your memories 🌄", + "newPhotosEmoji": " new 📸", + "sorryWeHadToPauseYourBackups": "Sorry, we had to pause your backups", + "clickToInstallOurBestVersionYet": "Click to install our best version yet", + "onThisDayNotificationExplanation": "Receive reminders about memories from this day in previous years.", + "birhtdayNotifications": "Birhtday notifications", + "receiveRemindersOnBirthdays": "Receive reminders when it's someone's birthday. Tapping on the notification will take you to photos of the birthday person.", + "happyBirthday": "Happy birthday! 🥳", + "birthdays": "Birthdays" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt_BR.arb b/mobile/lib/l10n/intl_pt_BR.arb index 90ccc351aa..3d6eff615a 100644 --- a/mobile/lib/l10n/intl_pt_BR.arb +++ b/mobile/lib/l10n/intl_pt_BR.arb @@ -1,17 +1,18 @@ { "@@locale ": "en", - "enterYourEmailAddress": "Insira seu endereço de e-mail", + "enterYourEmailAddress": "Insira seu e-mail", + "enterYourNewEmailAddress": "Insira seu novo e-mail", "accountWelcomeBack": "Bem-vindo(a) de volta!", "emailAlreadyRegistered": "E-mail já registrado.", "emailNotRegistered": "E-mail não registrado.", "email": "E-mail", "cancel": "Cancelar", "verify": "Verificar", - "invalidEmailAddress": "Endereço de e-mail inválido", - "enterValidEmail": "Insira um endereço de e-mail válido.", + "invalidEmailAddress": "E-mail inválido", + "enterValidEmail": "Insira um e-mail válido.", "deleteAccount": "Excluir conta", "askDeleteReason": "Por que você quer excluir sua conta?", - "deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar.", + "deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para ajudar-nos a melhorar.", "feedback": "Feedback", "kindlyHelpUsWithThisInformation": "Ajude-nos com esta informação", "confirmDeletePrompt": "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos.", @@ -131,13 +132,13 @@ } }, "twofactorSetup": "Configuração de dois fatores", - "enterCode": "Insira o código", + "enterCode": "Inserir código", "scanCode": "Escanear código", "codeCopiedToClipboard": "Código copiado para a área de transferência", - "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole este código\npara o aplicativo autenticador", + "copypasteThisCodentoYourAuthenticatorApp": "Copie e cole o código no aplicativo autenticador", "tapToCopy": "toque para copiar", "scanThisBarcodeWithnyourAuthenticatorApp": "Escaneie este código de barras com\no aplicativo autenticador", - "enterThe6digitCodeFromnyourAuthenticatorApp": "Digite o código de 6 dígitos do\naplicativo de autenticador", + "enterThe6digitCodeFromnyourAuthenticatorApp": "Digite o código de 6 dígitos do\naplicativo autenticador", "confirm": "Confirmar", "setupComplete": "Configuração concluída", "saveYourRecoveryKeyIfYouHaventAlready": "Salve sua chave de recuperação, se você ainda não fez", @@ -158,7 +159,7 @@ "addCollaborator": "Adicionar colaborador", "addANewEmail": "Adicionar um novo e-mail", "orPickAnExistingOne": "Ou escolha um existente", - "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.", + "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.", "enterEmail": "Inserir e-mail", "albumOwner": "Proprietário", "@albumOwner": { @@ -272,7 +273,7 @@ "shareTextRecommendUsingEnte": "Baixe o Ente para que nós possamos compartilhar com facilidade fotos e vídeos de qualidade original\n\nhttps://ente.io", "done": "Concluído", "applyCodeTitle": "Aplicar código", - "enterCodeDescription": "Insira o código fornecido pelo seu amigo para reivindicar o armazenamento grátis para os dois", + "enterCodeDescription": "Insira o código fornecido por um amigo para reivindicar armazenamento grátis para ambos", "apply": "Aplicar", "failedToApplyCode": "Falhou ao aplicar código", "enterReferralCode": "Inserir código de referência", @@ -290,14 +291,14 @@ "details": "Detalhes", "claimMore": "Reivindique mais!", "theyAlsoGetXGb": "Eles também recebem {storageAmountInGB} GB", - "freeStorageOnReferralSuccess": "{storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código", - "shareTextReferralCode": "Código de referência do Ente: {referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter {referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io", - "claimFreeStorage": "Reivindicar armazenamento grátis", + "freeStorageOnReferralSuccess": "{storageAmountInGB} GB toda vez que alguém utilizar seu código num plano pago", + "shareTextReferralCode": "Código de referência Ente: {referralCode} \n\nAplique-o em Opções → Geral → Referências para obter {referralStorageInGB} GB grátis após a inscrição num plano pago\n\nhttps://ente.io", + "claimFreeStorage": "Reivindique armaz. grátis", "inviteYourFriends": "Convide seus amigos", "failedToFetchReferralDetails": "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde.", "referralStep1": "1. Envie este código aos seus amigos", "referralStep2": "2. Eles então se inscrevem num plano pago", - "referralStep3": "3. Ambos os dois ganham {storageInGB} GB* grátis", + "referralStep3": "3. Ambos ganham {storageInGB} GB* grátis", "referralsAreCurrentlyPaused": "As referências estão atualmente pausadas", "youCanAtMaxDoubleYourStorage": "* Você pode duplicar seu armazenamento ao máximo", "claimedStorageSoFar": "{isFamilyMember, select,true {Sua família reinvidicou {storageAmountInGb} GB até então}false {Você reinvindicou {storageAmountInGb} GB até então}other {Você reinvindicou {storageAmountInGb} GB até então!}}", @@ -316,13 +317,13 @@ "faq": "Perguntas frequentes", "help": "Ajuda", "oopsSomethingWentWrong": "Ops, algo deu errado", - "peopleUsingYourCode": "Pessoas que usam seu código", + "peopleUsingYourCode": "Pessoas que usou o código", "eligible": "elegível", "total": "total", "codeUsedByYou": "Código usado por você", - "freeStorageClaimed": "Armazenamento grátis reivindicado", + "freeStorageClaimed": "Armaz. grátis reivindicado", "freeStorageUsable": "Armazenamento disponível", - "usableReferralStorageInfo": "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano.", + "usableReferralStorageInfo": "O armazenamento disponível é limitado devido ao seu plano atual. O armazenamento adicional será aplicado quando você atualizar seu plano.", "removeFromAlbumTitle": "Remover do álbum?", "removeFromAlbum": "Remover do álbum", "itemsWillBeRemovedFromAlbum": "Os itens selecionados serão removidos deste álbum", @@ -341,15 +342,15 @@ "yesRemove": "Sim, excluir", "creatingLink": "Criando link...", "removeWithQuestionMark": "Remover?", - "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum", + "removeParticipantBody": "{userEmail} será removido do álbum compartilhado\n\nQualquer foto adicionada por ele será removida.", "keepPhotos": "Manter fotos", "deletePhotos": "Excluir fotos", "inviteToEnte": "Convidar ao Ente", "removePublicLink": "Remover link público", "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", "sharing": "Compartilhando...", - "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivo", + "youCannotShareWithYourself": "Não é possível compartilhar consigo mesmo", + "archive": "Arquive", "createAlbumActionHint": "Pressione para selecionar fotos e clique em + para criar um álbum", "importing": "Importando....", "failedToLoadAlbums": "Falhou ao carregar álbuns", @@ -419,12 +420,12 @@ "photoGridSize": "Tamanho da grade de fotos", "manageDeviceStorage": "Gerenciar cache do dispositivo", "manageDeviceStorageDesc": "Reveja e limpe o armazenamento de cache local.", - "machineLearning": "Aprendizagem automática", - "mlConsent": "Ativar aprendizagem automática", - "mlConsentTitle": "Ativar aprendizagem automática?", - "mlConsentDescription": "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta.", + "machineLearning": "Aprendizado automático", + "mlConsent": "Ativar o aprendizado automático", + "mlConsentTitle": "Ativar aprendizado automático?", + "mlConsentDescription": "Se ativar o aprendizado automático, Ente extrairá informações de geometria facial dos arquivos, incluindo aqueles compartilhados consigo.\n\nIsso acontecerá em seu dispositivo, e qualquer informação biométrica gerada será criptografada de ponta a ponta.", "mlConsentPrivacy": "Clique aqui para mais detalhes sobre este recurso na política de privacidade", - "mlConsentConfirmation": "Eu entendo, e desejo ativar a aprendizagem automática", + "mlConsentConfirmation": "Concordo e desejo ativá-lo", "magicSearch": "Busca mágica", "discover": "Explorar", "@discover": { @@ -445,15 +446,15 @@ "discover_sunset": "Pôr do sol", "discover_hills": "Colinas", "discover_greenery": "Vegetação", - "mlIndexingDescription": "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.", + "mlIndexingDescription": "Saiba que o aprendizado automático afetará a bateria do dispositivo negativamente até todos os itens serem indexados. Utilize a versão para computadores para melhor indexação, todos os resultados se auto-sincronizaram.", "loadingModel": "Baixando modelos...", "waitingForWifi": "Aguardando Wi-Fi...", "status": "Estado", "indexedItems": "Itens indexados", "pendingItems": "Itens pendentes", "clearIndexes": "Limpar índices", - "selectFoldersForBackup": "Selecionar pastas para copiar com segurança", - "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão criptografadas e armazenadas em copiadas com segurança", + "selectFoldersForBackup": "Selecione as pastas para salvá-las", + "selectedFoldersWillBeEncryptedAndBackedUp": "As pastas selecionadas serão criptografadas e salvas em segurança", "unselectAll": "Desmarcar tudo", "selectAll": "Selecionar tudo", "skip": "Pular", @@ -477,13 +478,13 @@ }, "showMemories": "Mostrar memórias", "yearsAgo": "{count, plural, one{{count} ano atrás} other{{count} anos atrás}}", - "backupSettings": "Opções de cópia de segurança", - "backupStatus": "Status da cópia de segurança", - "backupStatusDescription": "Os itens que foram salvos com segurança aparecerão aqui", - "backupOverMobileData": "Salvamento com segurança usando dados móveis", - "backupVideos": "Cópia de segurança de vídeos", + "backupSettings": "Ajustes de salvar em segurança", + "backupStatus": "Estado das mídias salvas", + "backupStatusDescription": "Os itens salvos em segurança aparecerão aqui", + "backupOverMobileData": "Salvar em segurança com dados móveis", + "backupVideos": "Salvar vídeos em segurança", "disableAutoLock": "Desativar bloqueio automático", - "deviceLockExplanation": "Desativa o bloqueio de tela quando o Ente está de fundo e têm uma cópia de segurança sendo feita. Isso normalmente não é necessário, no entanto, ajuda a envios grandes e importações iniciais de bibliotecas maiores concluírem mais rápido.", + "deviceLockExplanation": "Desativa o bloqueio de tela se o Ente estiver de fundo e uma cópia de segurança ainda estar em andamento. Às vezes, isso não é necessário, mas ajuda a agilizar envios grandes e importações iniciais de bibliotecas maiores.", "about": "Sobre", "weAreOpenSource": "Nós somos de código aberto!", "privacy": "Privacidade", @@ -514,8 +515,8 @@ "cannotDeleteSharedFiles": "Não é possível excluir arquivos compartilhados", "theDownloadCouldNotBeCompleted": "A instalação não pôde ser concluída", "retry": "Tentar novamente", - "backedUpFolders": "Pastas copiadas com segurança", - "backup": "Cópia de segurança", + "backedUpFolders": "Pastas salvas em segurança", + "backup": "Salvar em segurança", "freeUpDeviceSpace": "Liberar espaço no dispositivo", "freeUpDeviceSpaceDesc": "Economize espaço em seu dispositivo por limpar arquivos já salvos com segurança.", "allClear": "✨ Tudo limpo", @@ -528,7 +529,7 @@ "youveNoDuplicateFilesThatCanBeCleared": "Você não possui nenhum arquivo duplicado que possa ser excluído", "success": "Sucesso", "rateUs": "Avaliar", - "remindToEmptyDeviceTrash": "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado", + "remindToEmptyDeviceTrash": "Também esvazie o \"Excluído Recentemente\" das \"Opções\" -> \"Armazenamento\" para liberar espaço", "youHaveSuccessfullyFreedUp": "Você liberou {storageSaved} com sucesso!", "@youHaveSuccessfullyFreedUp": { "description": "The text to display when the user has successfully freed up storage", @@ -597,7 +598,7 @@ "freeTrial": "Avaliação grátis", "selectYourPlan": "Selecione seu plano", "enteSubscriptionPitch": "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo.", - "enteSubscriptionShareWithFamily": "Sua família também pode ser adicionada ao seu plano.", + "enteSubscriptionShareWithFamily": "Sua família também poderá ser adicionada ao seu plano.", "currentUsageIs": "O uso atual é ", "@currentUsageIs": { "description": "This text is followed by storage usage", @@ -721,11 +722,12 @@ "type": "text" }, "backupFailed": "Falhou ao copiar com segurança", + "sorryBackupFailedDesc": "Desculpe, não podemos fazer cópia de segurança deste arquivo no momento, nós tentaremos mais tarde.", "couldNotBackUpTryLater": "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles", "pleaseGrantPermissions": "Por favor, conceda as permissões", "grantPermission": "Conceder permissões", - "privateSharing": "Compartilhamento privado", + "privateSharing": "Compartilha privada", "shareOnlyWithThePeopleYouWant": "Compartilhar apenas com as pessoas que você quiser", "usePublicLinksForPeopleNotOnEnte": "Usar links públicos para pessoas que não estão no Ente", "allowPeopleToAddPhotos": "Permitir que pessoas adicionem fotos", @@ -1029,7 +1031,7 @@ "didYouKnow": "Você sabia?", "loadingMessage": "Carregando suas fotos...", "loadMessage1": "Você pode compartilhar sua assinatura com seus familiares", - "loadMessage2": "Nós preservamos mais de 30 milhões de memórias até então", + "loadMessage2": "Preservamos mais de 200 milhões de memórias até então", "loadMessage3": "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo", "loadMessage4": "Todos os nossos aplicativos são de código aberto", "loadMessage5": "Nosso código-fonte e criptografia foram auditadas externamente", @@ -1280,6 +1282,8 @@ "createCollaborativeLink": "Criar link colaborativo", "search": "Buscar", "enterPersonName": "Inserir nome da pessoa", + "editEmailAlreadyLinked": "Este e-mail já está vinculado a {name}.", + "viewPersonToUnlink": "Visualizar {name} para desvincular", "enterName": "Inserir nome", "savePerson": "Salvar pessoa", "editPerson": "Editar pessoa", @@ -1328,7 +1332,7 @@ "enable": "Ativar", "enabled": "Ativado", "moreDetails": "Mais detalhes", - "enableMLIndexingDesc": "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados", + "enableMLIndexingDesc": "Ente fornece aprendizado automático no dispositivo para reconhecimento facial, busca mágica e outros recursos de busca avançados.", "magicSearchHint": "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. 'flor', 'carro vermelho', 'identidade'", "panorama": "Panorama", "reenterPassword": "Reinserir senha", @@ -1397,7 +1401,7 @@ "yesResetPerson": "Sim, redefinir pessoa", "onlyThem": "Apenas eles", "checkingModels": "Verificando modelos...", - "enableMachineLearningBanner": "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial", + "enableMachineLearningBanner": "Ativar o aprendizado automático para busca mágica e reconhecimento facial", "searchDiscoverEmptySection": "As imagens serão exibidas aqui quando o processamento e sincronização for concluído", "searchPersonsEmptySection": "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído", "viewersSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 vizualizadores} =1 {Adicionado 1 visualizador} other {Adicionado {count} visualizadores}}", @@ -1655,11 +1659,11 @@ "dontSave": "Não salvar", "thisIsMeExclamation": "Este é você!", "linkPerson": "Vincular pessoa", - "linkPersonCaption": "para melhor experiência de compartilhamento", + "linkPersonCaption": "para melhorar o compartilhamento", "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Transmissão de vídeo", + "videoStreaming": "Vídeos transmissíveis", "processingVideos": "Processando vídeos", "streamDetails": "Detalhes da transmissão", "processing": "Processando", @@ -1711,7 +1715,7 @@ "roadtripWithThem": "Viajando de carro com {name}", "spotlightOnYourself": "Destacar si mesmo", "spotlightOnThem": "Destacar {name}", - "personIsAge": "{name} está com {age}!", + "personIsAge": "{name} tem {age} anos!", "personTurningAge": "{name} terá {age} em breve", "lastTimeWithThem": "Últimos momentos com {name}", "tripToLocation": "Viajem à {location}", @@ -1723,18 +1727,34 @@ "beach": "Areia e o mar", "city": "Na cidade", "moon": "Na luz do luar", - "onTheRoad": "Na estrada de novo", - "food": "Prazer em culinária", - "pets": "Companhia de pelos", - "cLIcon": "Novo Ícone", - "cLIconDesc": "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo.", - "cLMemories": "Memórias", - "cLMemoriesDesc": "Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência.", - "cLWidgets": "Widgets", - "cLWidgetsDesc": "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente.", - "cLFamilyPlan": "Limites de planos familiares", - "cLFamilyPlanDesc": "Agora você pode definir um limite de quanto armazenamento os seus entes queridos podem usar.", - "cLBulkEdit": "Editar todas as datas", - "cLBulkEditDesc": "Agora você pode selecionar várias fotos, editar data e hora de todos com um só clique. Alternar datas também são suportados.", - "curatedMemories": "Memórias restauradas" + "onTheRoad": "Na estrada novamente", + "food": "Amor por culinária", + "pets": "Companhias peludas", + "curatedMemories": "Memórias restauradas", + "widgets": "Widgets", + "memories": "Memórias", + "peopleWidgetDesc": "Selecione as pessoas que deseje vê-las na sua tela inicial.", + "albumsWidgetDesc": "Selecione os álbuns que deseje vê-los na sua tela inicial.", + "memoriesWidgetDesc": "Selecione os tipos de memórias que deseje vê-las na sua tela inicial.", + "smartMemories": "Memórias inteligentes", + "pastYearsMemories": "Memórias dos anos passados", + "deleteMultipleAlbumDialog": "E também excluir todas as fotos (e vídeos) presente dentro desses {count} álbuns e de todos os álbuns que eles fazem parte?", + "addParticipants": "Adicionar participante", + "selectedAlbums": "{count} selecionado(s)", + "actionNotSupportedOnFavouritesAlbum": "Ação não suportada em álbum favorito", + "onThisDayMemories": "Memórias deste dia", + "onThisDay": "Neste dia", + "lookBackOnYourMemories": "Revisão em suas memórias 🌄", + "newPhotosEmoji": " novo 📸", + "sorryWeHadToPauseYourBackups": "Desculpe, tivemos que pausar suas cópias de segurança", + "clickToInstallOurBestVersionYet": "Clique para instalar a nossa melhor versão até então", + "onThisDayNotificationExplanation": "Receba lembretes de memórias deste dia em anos passados.", + "addMemoriesWidgetPrompt": "Adicione um widget de memória a sua tela inicial e volte aqui para personalizar.", + "addAlbumWidgetPrompt": "Adicione um widget de álbum a sua tela inicial e volte aqui para personalizar.", + "addPeopleWidgetPrompt": "Adicione um widget de pessoas a sua tela inicial e volte aqui para personalizar.", + "birthdayNotifications": "Notificações de aniversário", + "receiveRemindersOnBirthdays": "Receba notificações quando alguém fizer um aniversário. Tocar na notificação o levará às fotos do aniversariante.", + "happyBirthday": "Feliz aniversário! 🥳", + "happyBirthdayToPerson": "Feliz aniversário a {name}! 🎉", + "birthdays": "Aniversários" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt_PT.arb b/mobile/lib/l10n/intl_pt_PT.arb index 662730e770..b1bd6e5ac2 100644 --- a/mobile/lib/l10n/intl_pt_PT.arb +++ b/mobile/lib/l10n/intl_pt_PT.arb @@ -237,7 +237,7 @@ "publicLinkEnabled": "Link público ativado", "shareALink": "Partilhar um link", "sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos.", - "shareWithPeopleSectionTitle": "{numberOfPeople, plural, one {}=0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", + "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}", "@shareWithPeopleSectionTitle": { "placeholders": { "numberOfPeople": { @@ -984,7 +984,6 @@ "didYouKnow": "Você sabia?", "loadingMessage": "Carregar as suas fotos...", "loadMessage1": "Pode partilhar a sua subscrição com a sua família", - "loadMessage2": "Nós preservamos mais de 30 milhões de memórias até agora", "loadMessage3": "Mantemos 3 cópias dos seus dados, uma em um abrigo subterrâneo", "loadMessage4": "Todos os nossos aplicativos são de código aberto", "loadMessage5": "Nosso código-fonte e criptografia foram auditadas externamente", diff --git a/mobile/lib/l10n/intl_ro.arb b/mobile/lib/l10n/intl_ro.arb index 9f481d2130..6cc353ab2b 100644 --- a/mobile/lib/l10n/intl_ro.arb +++ b/mobile/lib/l10n/intl_ro.arb @@ -992,7 +992,6 @@ "didYouKnow": "Știați că?", "loadingMessage": "Se încarcă fotografiile...", "loadMessage1": "Puteți împărți abonamentul cu familia dvs.", - "loadMessage2": "Am păstrat până acum peste 30 de milioane de amintiri", "loadMessage3": "Păstrăm 3 copii ale datelor dvs., dintre care una într-un adăpost antiatomic subteran", "loadMessage4": "Toate aplicațiile noastre sunt open source", "loadMessage5": "Codul nostru sursă și criptografia au fost evaluate extern", diff --git a/mobile/lib/l10n/intl_ru.arb b/mobile/lib/l10n/intl_ru.arb index c3188d96bb..98bcdac583 100644 --- a/mobile/lib/l10n/intl_ru.arb +++ b/mobile/lib/l10n/intl_ru.arb @@ -220,7 +220,7 @@ "after1Month": "Через 1 месяц", "after1Year": "Через 1 год", "manageParticipants": "Управлять", - "albumParticipantsCount": "{count, plural, =0 {Нет участников} =1 {{count} участник} one {{count} участник} few {{count} участника} other {{count} участников}}", + "albumParticipantsCount": "{count, plural, =0 {Нет участников} =1 {{count} участник} other {{count} участников}}", "@albumParticipantsCount": { "placeholders": { "count": { @@ -371,7 +371,7 @@ "deleteFromBoth": "Удалить из обоих мест", "newAlbum": "Новый альбом", "albums": "Альбомы", - "memoryCount": "{count, plural, =0{нет воспоминаний} one{{formattedCount} воспоминание} few{{formattedCount} воспоминания} other{{formattedCount} воспоминаний}}", + "memoryCount": "{count, plural, =0{нет воспоминаний} one{{formattedCount} воспоминание} other{{formattedCount} воспоминаний}}", "@memoryCount": { "description": "The text to display the number of memories", "type": "text", @@ -458,8 +458,8 @@ "selectAll": "Выбрать все", "skip": "Пропустить", "updatingFolderSelection": "Обновление выбора папок...", - "itemCount": "{count, plural, one{{count} элемент} few{{count} элемента} other{{count} элементов}}", - "deleteItemCount": "{count, plural, =1 {Удалить {count} элемент} one {Удалить {count} элемент} few {Удалить {count} элемента} other {Удалить {count} элементов}}", + "itemCount": "{count, plural, one{{count} элемент} other{{count} элементов}}", + "deleteItemCount": "{count, plural, =1 {Удалить {count} элемент} other {Удалить {count} элементов}}", "duplicateItemsGroup": "{count} файлов, по {formattedSize} каждый", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -475,7 +475,7 @@ } } }, - "showMemories": "Показать воспоминания", + "showMemories": "Показывать воспоминания", "yearsAgo": "{count, plural, one{{count} год назад} few{{count} года назад} other{{count} лет назад}}", "backupSettings": "Настройки резервного копирования", "backupStatus": "Статус резервного копирования", @@ -542,7 +542,7 @@ }, "remindToEmptyEnteTrash": "Также очистите «Корзину», чтобы освободить место", "sparkleSuccess": "✨ Успех", - "duplicateFileCountWithStorageSaved": "Вы удалили {count, plural, one{{count} дубликат} few{{count} дубликата} other{{count} дубликатов}}, освободив ({storageSaved}!)", + "duplicateFileCountWithStorageSaved": "Вы удалили {count, plural, one{{count} дубликат} other{{count} дубликатов}}, освободив ({storageSaved}!)", "@duplicateFileCountWithStorageSaved": { "description": "The text to display when the user has successfully cleaned up duplicate files", "type": "text", @@ -566,7 +566,7 @@ "general": "Общие", "security": "Безопасность", "authToViewYourRecoveryKey": "Пожалуйста, авторизуйтесь для просмотра ключа восстановления", - "twofactor": "Двухфакторная", + "twofactor": "Двухфакторная аутентификация", "authToConfigureTwofactorAuthentication": "Пожалуйста, авторизуйтесь для настройки двухфакторной аутентификации", "lockscreen": "Экран блокировки", "authToChangeLockscreenSetting": "Пожалуйста, авторизуйтесь для изменения настроек экрана блокировки", @@ -824,7 +824,7 @@ "referFriendsAnd2xYourPlan": "Пригласите друзей и удвойте свой тариф", "shareAlbumHint": "Откройте альбом и нажмите кнопку «Поделиться» в правом верхнем углу, чтобы поделиться.", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "На элементах отображается количество дней, оставшихся до их безвозвратного удаления", - "trashDaysLeft": "{count, plural, =0 {Скоро} =1 {1 день} one{{count} день} few{{count} дня} other {{count} дней}}", + "trashDaysLeft": "{count, plural, =0 {Скоро} =1 {1 день} other {{count} дней}}", "@trashDaysLeft": { "description": "Text to indicate number of days remaining before permanent deletion", "placeholders": { @@ -898,7 +898,7 @@ "unlock": "Разблокировать", "freeUpSpace": "Освободить место", "freeUpSpaceSaving": "{count, plural, =1 {Его можно удалить с устройства, чтобы освободить {formattedSize}} other {Их можно удалить с устройства, чтобы освободить {formattedSize}}}", - "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} файл в этом альбоме был успешно сохранён} few{{formattedNumber} файла в этом альбоме были успешно сохранены} other {{formattedNumber} файлов в этом альбоме были успешно сохранены}}", + "filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} файл в этом альбоме был успешно сохранён} other {{formattedNumber} файлов в этом альбоме были успешно сохранены}}", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -913,7 +913,7 @@ } } }, - "filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} файл на этом устройстве был успешно сохранён} few{{formattedNumber} файла на этом устройстве были успешно сохранены} other {{formattedNumber} файлов на этом устройстве были успешно сохранены}}", + "filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} файл на этом устройстве был успешно сохранён} other {{formattedNumber} файлов на этом устройстве были успешно сохранены}}", "@filesBackedUpFromDevice": { "description": "Text to tell user how many files have been backed up from this device", "placeholders": { @@ -1029,7 +1029,6 @@ "didYouKnow": "Знаете ли вы?", "loadingMessage": "Загрузка ваших фото...", "loadMessage1": "Вы можете поделиться подпиской с вашей семьёй", - "loadMessage2": "Мы сохранили уже более 30 миллионов воспоминаний", "loadMessage3": "Мы храним 3 копии ваших данных, одну из них — в бункере", "loadMessage4": "Все наши приложения имеют открытый исходный код", "loadMessage5": "Наш исходный код и криптография прошли внешний аудит", @@ -1049,7 +1048,7 @@ "searchFileTypesAndNamesEmptySection": "Типы и названия файлов", "searchCaptionEmptySection": "Добавляйте описания вроде «#поездка» в информацию о фото, чтобы быстро находить их здесь", "language": "Язык", - "selectLanguage": "Выбрать язык", + "selectLanguage": "Выберите язык", "locationName": "Название местоположения", "addLocation": "Добавить местоположение", "groupNearbyPhotos": "Группировать ближайшие фото", @@ -1215,7 +1214,7 @@ "searchHint4": "Местоположение", "searchHint5": "Скоро: Лица и магический поиск ✨", "addYourPhotosNow": "Добавьте ваши фото", - "searchResultCount": "{count, plural, one{{count} результат найден} few{{count} результата найдено} other {{count} результатов найдено}}", + "searchResultCount": "{count, plural, one{{count} результат найден} other {{count} результатов найдено}}", "@searchResultCount": { "description": "Text to tell user how many results were found for their search query", "placeholders": { @@ -1336,7 +1335,7 @@ "deviceLock": "Блокировка устройства", "pinLock": "Блокировка PIN-кодом", "next": "Далее", - "setNewPassword": "Установить новый пароль", + "setNewPassword": "Установите новый пароль", "enterPin": "Введите PIN-код", "setNewPin": "Установите новый PIN-код", "appLock": "Блокировка приложения", @@ -1410,7 +1409,7 @@ }, "description": "Number of viewers that were successfully added to an album." }, - "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Добавлено 0 соавторов} =1 {Добавлен 1 соавтор} one{Добавлен {count} соавтор} few{Добавлено {count} соавтора} other {Добавлено {count} соавторов}}", + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {Добавлено 0 соавторов} =1 {Добавлен 1 соавтор} other {Добавлено {count} соавторов}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1485,7 +1484,7 @@ }, "currentlyRunning": "выполняется", "ignored": "игнорируется", - "photosCount": "{count, plural, =0 {0 фотографий} =1 {1 фотография} one {{count} фотография} few {{count} фотографии} other {{count} фотографий}}", + "photosCount": "{count, plural, =0 {0 фотографий} =1 {1 фотография} other {{count} фотографий}}", "@photosCount": { "placeholders": { "count": { @@ -1659,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Потоковое видео", "processingVideos": "Обработка видео", "streamDetails": "Информация о потоке", "processing": "Обработка", @@ -1697,7 +1695,7 @@ "selectedItemsWillBeRemovedFromThisPerson": "Выбранные элементы будут отвязаны от этого человека, но не удалены из вашей библиотеки.", "throughTheYears": "{dateFormat} сквозь годы", "thisWeekThroughTheYears": "Эта неделя сквозь годы", - "thisWeekXYearsAgo": "{count, plural, =1 {Эта неделя, {count} год назад} one{Эта неделя, {count} год назад} few{Эта неделя, {count} года назад} other {Эта неделя, {count} лет назад}}", + "thisWeekXYearsAgo": "{count, plural, =1 {Эта неделя, {count} год назад} other {Эта неделя, {count} лет назад}}", "youAndThem": "Вы и {name}", "admiringThem": "Любуясь {name}", "embracingThem": "Обнимая {name}", @@ -1726,14 +1724,5 @@ "onTheRoad": "Снова в пути", "food": "Кулинарное наслаждение", "pets": "Пушистые спутники", - "cLIcon": "Новая иконка", - "cLIconDesc": "Наконец-то новая иконка приложения, которая, как мы считаем, лучше всего отражает нашу работу. Мы также добавили переключатель иконок, чтобы вы могли продолжать использовать старую иконку.", - "cLMemories": "Воспоминания", - "cLMemoriesDesc": "Откройте заново свои особенные моменты — в центре внимания ваши любимые люди, поездки и праздники, лучшие снимки и многое другое. Для наилучших впечатлений включите машинное обучение и отметьте себя и своих друзей.", - "cLWidgets": "Виджеты", - "cLWidgetsDesc": "Теперь доступны виджеты домашнего экрана, интегрированные с воспоминаниями. Они покажут ваши особенные моменты, не открывая приложения.", - "cLFamilyPlan": "Ограничения семейного тарифа", - "cLFamilyPlanDesc": "Теперь вы можете установить ограничения на объём хранилища, которое могут использовать члены вашей семьи.", - "cLBulkEdit": "Массовое редактирование дат", - "cLBulkEditDesc": "Теперь вы можете выбрать несколько фото и отредактировать дату/время быстро и сразу для всех. Также поддерживается смещение дат." + "curatedMemories": "Отобранные воспоминания" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_sr.arb b/mobile/lib/l10n/intl_sr.arb new file mode 100644 index 0000000000..c8494661c6 --- /dev/null +++ b/mobile/lib/l10n/intl_sr.arb @@ -0,0 +1,3 @@ +{ + "@@locale ": "en" +} \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ta.arb b/mobile/lib/l10n/intl_ta.arb index d3d26e203c..1b9d1e8190 100644 --- a/mobile/lib/l10n/intl_ta.arb +++ b/mobile/lib/l10n/intl_ta.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "உங்கள் மின்னஞ்சல் முகவரியை உள்ளிடவும்", "accountWelcomeBack": "மீண்டும் வருக!", + "emailAlreadyRegistered": "மின்னஞ்சல் முன்பே பதிவுசெய்யப்பட்டுள்ளது.", + "emailNotRegistered": "மின்னஞ்சல் பதிவு செய்யப்படவில்லை.", "email": "மின்னஞ்சல்", "cancel": "ரத்து செய்", "verify": "சரிபார்க்கவும்", @@ -15,5 +17,7 @@ "confirmDeletePrompt": "ஆம், எல்லா செயலிகளிலும் இந்தக் கணக்கையும் அதன் தரவையும் நிரந்தரமாக நீக்க விரும்புகிறேன்.", "confirmAccountDeletion": "கணக்கு நீக்குதலை உறுதிப்படுத்தவும்", "deleteAccountPermanentlyButton": "கணக்கை நிரந்தரமாக நீக்கவும்", + "yourAccountHasBeenDeleted": "உங்கள் கணக்கு நீக்கப்பட்டது", + "selectReason": "காரணத்தைத் தேர்ந்தெடுக்கவும்", "deleteReason1": "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_tr.arb b/mobile/lib/l10n/intl_tr.arb index 5e01c9d1d4..37294ba1e4 100644 --- a/mobile/lib/l10n/intl_tr.arb +++ b/mobile/lib/l10n/intl_tr.arb @@ -1,17 +1,18 @@ { "@@locale ": "en", "enterYourEmailAddress": "E-posta adresinizi girin", + "enterYourNewEmailAddress": "Yeni e-posta adresinizi girin", "accountWelcomeBack": "Tekrar hoş geldiniz!", "emailAlreadyRegistered": "Bu e-posta adresi zaten kayıtlı.", "emailNotRegistered": "Bu e-posta adresi sistemde kayıtlı değil.", "email": "E-Posta", - "cancel": "İptal Et", + "cancel": "İptal et", "verify": "Doğrula", "invalidEmailAddress": "Geçersiz e-posta adresi", "enterValidEmail": "Lütfen geçerli bir E-posta adresi girin.", "deleteAccount": "Hesabı sil", - "askDeleteReason": "Hesabınızı neden silmek istiyorsunuz?", - "deleteAccountFeedbackPrompt": "Aramızdan ayrıldığınız için üzgünüz. Lütfen kendimizi geliştirmemize yardımcı olun. Neden ayrıldığınızı Açıklar mısınız.", + "askDeleteReason": "Hesabınızı silme sebebiniz nedir?", + "deleteAccountFeedbackPrompt": "Gittiğini gördüğümüze üzüldük. Lütfen gelişmemize yardımcı olmak için neden ayrıldığınızı açıklayın.", "feedback": "Geri Bildirim", "kindlyHelpUsWithThisInformation": "Lütfen bu bilgilerle bize yardımcı olun", "confirmDeletePrompt": "Evet, bu hesabı ve verilerini tüm uygulamalardan kalıcı olarak silmek istiyorum.", @@ -25,7 +26,7 @@ "deleteReason4": "Nedenim listede yok", "sendEmail": "E-posta gönder", "deleteRequestSLAText": "İsteğiniz 72 saat içinde gerçekleştirilecek.", - "deleteEmailRequest": "Lütfen kayıtlı e-posta adresinizden account-deletion@ente.io'a e-posta gönderiniz.", + "deleteEmailRequest": "Lütfen kayıtlı e-posta adresinizden account-deletion@ente.io'ya e-posta gönderiniz.", "entePhotosPerm": "Ente fotoğrafları saklamak için iznine ihtiyaç duyuyor", "ok": "Tamam", "createAccount": "Hesap oluşturun", @@ -37,7 +38,7 @@ "somethingWentWrongPleaseTryAgain": "Bir şeyler ters gitti, lütfen tekrar deneyin", "thisWillLogYouOutOfThisDevice": "Bu cihazdaki oturumunuz kapatılacak!", "thisWillLogYouOutOfTheFollowingDevice": "Bu, sizi aşağıdaki cihazdan çıkış yapacak:", - "terminateSession": "Oturumu sonlandır?", + "terminateSession": "Oturum sonlandırılsın mı?", "terminate": "Sonlandır", "thisDevice": "Bu cihaz", "recoverButton": "Kurtar", @@ -47,7 +48,7 @@ "incorrectRecoveryKeyBody": "Girdiğiniz kurtarma kod yanlış", "forgotPassword": "Şifremi unuttum", "enterYourRecoveryKey": "Kurtarma kodunuzu girin", - "noRecoveryKey": "Kurtarma kodunuz yok mu?", + "noRecoveryKey": "Kurtarma anahtarınız yok mu?", "sorry": "Üzgünüz", "noRecoveryKeyNoDecryption": "Uçtan uca şifreleme protokolümüzün doğası gereği, verileriniz şifreniz veya kurtarma anahtarınız olmadan çözülemez", "verifyEmail": "E-posta adresini doğrulayın", @@ -130,7 +131,7 @@ } } }, - "twofactorSetup": "Cift faktör ayarı", + "twofactorSetup": "İki faktörlü kurulum", "enterCode": "Kodu giriniz", "scanCode": "Kodu tarayın", "codeCopiedToClipboard": "Kodunuz panoya kopyalandı", @@ -143,7 +144,7 @@ "saveYourRecoveryKeyIfYouHaventAlready": "Henüz yapmadıysanız kurtarma anahtarınızı kaydetmeyi unutmayın", "thisCanBeUsedToRecoverYourAccountIfYou": "Bu, iki faktörünüzü kaybederseniz hesabınızı kurtarmak için kullanılabilir", "twofactorAuthenticationPageTitle": "İki faktörlü doğrulama", - "lostDevice": "Cihazı kayıp mı ettiniz?", + "lostDevice": "Cihazınızı mı kaybettiniz?", "verifyingRecoveryKey": "Kurtarma kodu doğrulanıyor...", "recoveryKeyVerified": "Kurtarma kodu doğrulandı", "recoveryKeySuccessBody": "Harika! Kurtarma anahtarınız geçerlidir. Doğrulama için teşekkür ederim.\n\nLütfen kurtarma anahtarınızı güvenli bir şekilde yedeklediğinizden emin olun.", @@ -185,19 +186,19 @@ "@allowAddingPhotos": { "description": "Switch button to enable uploading photos to a public link" }, - "allowAddPhotosDescription": "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver.", - "passwordLock": "Sifre kilidi", + "allowAddPhotosDescription": "Bağlantıya sahip olan kişilerin paylaşılan albüme fotoğraf eklemelerine izin ver.", + "passwordLock": "Şifre kilidi", "canNotOpenTitle": "Albüm açılamadı", "canNotOpenBody": "Üzgünüz, Bu albüm uygulama içinde açılamadı.", "disableDownloadWarningTitle": "Lütfen dikkate alın", "disableDownloadWarningBody": "Görüntüleyiciler, hala harici araçlar kullanarak ekran görüntüsü alabilir veya fotoğraflarınızın bir kopyasını kaydedebilir. Lütfen bunu göz önünde bulundurunuz", "allowDownloads": "İndirmeye izin ver", - "linkDeviceLimit": "Cihaz limiti", + "linkDeviceLimit": "Cihaz sınırı", "noDeviceLimit": "Yok", "@noDeviceLimit": { "description": "Text to indicate that there is limit on number of devices" }, - "linkExpiry": "Linkin geçerliliği", + "linkExpiry": "Bağlantı geçerliliği", "linkExpired": "Süresi dolmuş", "linkEnabled": "Geçerli", "linkNeverExpires": "Asla", @@ -205,12 +206,12 @@ "setAPassword": "Şifre ayarla", "lockButtonLabel": "Kilit", "enterPassword": "Şifrenizi girin", - "removeLink": "Linki kaldır", - "manageLink": "Linki yönet", - "linkExpiresOn": "Bu bağlantı {expiryTime} dan sonra geçersiz olacaktır", + "removeLink": "Bağlantıyı kaldır", + "manageLink": "Bağlantıyı yönet", + "linkExpiresOn": "Bu bağlantı {expiryTime} tarihinden itibaren geçersiz olacaktır", "albumUpdated": "Albüm güncellendi", "never": "Asla", - "custom": "Kişisel", + "custom": "Özel", "@custom": { "description": "Label for setting custom value for link expiry" }, @@ -232,14 +233,14 @@ }, "collabLinkSectionDescription": "Ente aplikasyonu veya hesabı olmadan insanların paylaşılan albümde fotoğraf ekleyip görüntülemelerine izin vermek için bir bağlantı oluşturun. Grup veya etkinlik fotoğraflarını toplamak için harika bir seçenek.", "collectPhotos": "Fotoğrafları topla", - "collaborativeLink": "Organizasyon bağlantısı", + "collaborativeLink": "Ortak bağlantı", "shareWithNonenteUsers": "Ente kullanıcısı olmayanlar için paylaş", - "createPublicLink": "Herkese açık link oluştur", - "sendLink": "Link gönder", - "copyLink": "Linki kopyala", + "createPublicLink": "Herkese açık bir bağlantı oluştur", + "sendLink": "Bağlantıyı gönder", + "copyLink": "Bağlantıyı kopyala", "linkHasExpired": "Bağlantının süresi dolmuş", "publicLinkEnabled": "Herkese açık bağlantı aktive edildi", - "shareALink": "Linki paylaş", + "shareALink": "Bir bağlantı paylaş", "sharedAlbumSectionDescription": "Diğer Ente kullanıcılarıyla paylaşılan ve topluluk albümleri oluşturun, bu arada ücretsiz planlara sahip kullanıcıları da içerir.", "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Belirli kişilerle paylaş} =1 {1 kişiyle paylaşıldı} other {{numberOfPeople} kişiyle paylaşıldı}}", "@shareWithPeopleSectionTitle": { @@ -272,7 +273,7 @@ "shareTextRecommendUsingEnte": "Orijinal kalitede fotoğraf ve videoları kolayca paylaşabilmemiz için Ente'yi indirin\n\nhttps://ente.io", "done": "Bitti", "applyCodeTitle": "Kodu girin", - "enterCodeDescription": "Arkadaşınız tarafından sağlanan kodu girerek hem sizin hem de arkadaşınızın ücretsiz depolamayı talep etmek için girin", + "enterCodeDescription": "İkiniz için de ücretsiz depolama alanı talep etmek için arkadaşınız tarafından sağlanan kodu girin", "apply": "Uygula", "failedToApplyCode": "Uygulanırken hata oluştu", "enterReferralCode": "Davet kodunuzu girin", @@ -292,12 +293,12 @@ "theyAlsoGetXGb": "Aynı zamanda {storageAmountInGB} GB alıyorlar", "freeStorageOnReferralSuccess": "Birisinin davet kodunuzu uygulayıp ücretli hesap açtığı her seferede {storageAmountInGB} GB", "shareTextReferralCode": "Ente davet kodu: {referralCode} \n\nÜcretli hesaba başvurduktan sonra {referralStorageInGB} GB bedava almak için \nAyarlar → Genel → Davetlerde bu kodu girin\n\nhttps://ente.io", - "claimFreeStorage": "Bedava alan talep edin", + "claimFreeStorage": "Bedava alan kazanın", "inviteYourFriends": "Arkadaşlarını davet et", "failedToFetchReferralDetails": "Davet ayrıntıları çekilemedi. Iütfen daha sonra deneyin.", "referralStep1": "1. Bu kodu arkadaşlarınıza verin", "referralStep2": "2. Ücretli bir plan için kaydolsunlar", - "referralStep3": "3. Hepimiz {storageInGB} GB* bedava alın", + "referralStep3": "3. İkinizde bedava {storageInGB} GB alın", "referralsAreCurrentlyPaused": "Davetler şu anda durmuş durumda", "youCanAtMaxDoubleYourStorage": "* Alanınızı en fazla ikiye katlayabilirsiniz", "claimedStorageSoFar": "{isFamilyMember, select, true {Şu ana kadar aileniz {storageAmountInGb} GB aldı} false {Şu ana kadar {storageAmountInGb} GB aldınız} other {Şu ana kadar {storageAmountInGb} GB aldınız!}}", @@ -313,7 +314,7 @@ } } }, - "faq": "Sıkça sorulan sorular", + "faq": "Sık sorulan sorular", "help": "Yardım", "oopsSomethingWentWrong": "Hoop, Birşeyler yanlış gitti", "peopleUsingYourCode": "Kodunuzu kullananlar", @@ -340,12 +341,12 @@ "deleteSharedAlbumDialogBody": "Albüm herkes için silinecek\n\nBu albümdeki başkalarına ait paylaşılan fotoğraflara erişiminizi kaybedeceksiniz", "yesRemove": "Evet, sil", "creatingLink": "Bağlantı oluşturuluyor...", - "removeWithQuestionMark": "Kaldır?", + "removeWithQuestionMark": "Kaldırılsın mı?", "removeParticipantBody": "{userEmail} bu paylaşılan albümden kaldırılacaktır\n\nOnlar tarafından eklenen tüm fotoğraflar da albümden kaldırılacaktır", "keepPhotos": "Fotoğrafları sakla", "deletePhotos": "Fotoğrafları sil", "inviteToEnte": "Ente'ye davet edin", - "removePublicLink": "Herkese açık link oluştur", + "removePublicLink": "Herkese açık bağlantıyı kaldır", "disableLinkMessage": "Bu, \"{albumName}\"e erişim için olan genel bağlantıyı kaldıracaktır.", "sharing": "Paylaşılıyor...", "youCannotShareWithYourself": "Kendinizle paylaşamazsınız", @@ -371,6 +372,21 @@ "deleteFromBoth": "Her ikisinden de sil", "newAlbum": "Yeni albüm", "albums": "Albümler", + "memoryCount": "{count, plural, =0{hiç anı yok} one{{formattedCount} anı} other{{formattedCount} anı}}", + "@memoryCount": { + "description": "The text to display the number of memories", + "type": "text", + "placeholders": { + "count": { + "example": "1", + "type": "int" + }, + "formattedCount": { + "type": "String", + "example": "11.513, 11,511" + } + } + }, "selectedPhotos": "{count} seçildi", "@selectedPhotos": { "description": "Display the number of selected photos", @@ -401,8 +417,8 @@ "@advancedSettings": { "description": "The text to display in the advanced settings section" }, - "photoGridSize": "Fotoğraf ızgara boyutu", - "manageDeviceStorage": "Cihaz önbelliğini yönet", + "photoGridSize": "Izgara boyutu", + "manageDeviceStorage": "Cihaz Önbelliğini Yönet", "manageDeviceStorageDesc": "Yerel önbellek depolama alanını gözden geçirin ve temizleyin.", "machineLearning": "Makine öğrenimi", "mlConsent": "Makine öğrenimini etkinleştir", @@ -430,13 +446,13 @@ "discover_sunset": "Gün batımı", "discover_hills": "Tepeler", "discover_greenery": "Yeşillik", - "mlIndexingDescription": "Tüm öğeler dizine eklenene kadar makine öğreniminin daha yüksek bant genişliği ve pil kullanımı ile sonuçlanacağını lütfen unutmayın. Daha hızlı indeksleme için masaüstü uygulamasını kullanmayı düşünün, tüm sonuçlar otomatik olarak senkronize edilecektir.", + "mlIndexingDescription": "Makine öğreniminin, tüm öğeler dizine eklenene kadar daha yüksek bant genişliği ve pil kullanımıyla sonuçlanacağını lütfen unutmayın. Daha hızlı dizinleme için masaüstü uygulamasını kullanmayı deneyin, tüm sonuçlar otomatik olarak senkronize edilir.", "loadingModel": "Modeller indiriliyor...", "waitingForWifi": "WiFi bekleniyor...", "status": "Durum", - "indexedItems": "Yeni öğeleri indeksle", + "indexedItems": "Dizinlenmiş öğeler", "pendingItems": "Bekleyen Öğeler", - "clearIndexes": "Açık Dizin", + "clearIndexes": "Dizinleri temizle", "selectFoldersForBackup": "Yedekleme için klasörleri seçin", "selectedFoldersWillBeEncryptedAndBackedUp": "Seçilen klasörler şifrelenecek ve yedeklenecektir", "unselectAll": "Tümünün seçimini kaldır", @@ -466,7 +482,7 @@ "backupStatus": "Yedekleme durumu", "backupStatusDescription": "Eklenen öğeler burada görünecek", "backupOverMobileData": "Mobil veri ile yedekle", - "backupVideos": "Videolari yedekle", + "backupVideos": "Videoları yedekle", "disableAutoLock": "Otomatik kilidi devre dışı bırak", "deviceLockExplanation": "Ente uygulaması önplanda calıştığında ve bir yedekleme işlemi devam ettiğinde, cihaz ekran kilidini devre dışı bırakın. Bu genellikle gerekli olmasa da, büyük dosyaların yüklenmesi ve büyük kütüphanelerin başlangıçta içe aktarılması sürecini hızlandırabilir.", "about": "Hakkında", @@ -503,17 +519,17 @@ "backup": "Yedekle", "freeUpDeviceSpace": "Cihaz alanını boşaltın", "freeUpDeviceSpaceDesc": "Zaten yedeklenmiş dosyaları temizleyerek cihazınızda yer kazanın.", - "allClear": "✨ Tamamen temizle", - "noDeviceThatCanBeDeleted": "Bu cihazda silinebilecek hiçbir dosyanız yok", + "allClear": "✨ Tümü temizlendi", + "noDeviceThatCanBeDeleted": "Her şey zaten temiz, silinecek dosya kalmadı", "removeDuplicates": "Yinelenenleri kaldır", - "removeDuplicatesDesc": "Tam olarak yinelenen dosyaları gözden geçirin ve kaldırın.", + "removeDuplicatesDesc": "Aynı olan dosyaları gözden geçirin ve kaldırın.", "viewLargeFiles": "Büyük dosyalar", - "viewLargeFilesDesc": "En fazla depolama alanı tüketen dosyaları görüntüleyin.", + "viewLargeFilesDesc": "En fazla depolama alanı kullanan dosyaları görüntüleyin.", "noDuplicates": "Yinelenenleri kaldır", "youveNoDuplicateFilesThatCanBeCleared": "Temizlenebilecek yinelenen dosyalarınız yok", "success": "Başarılı", "rateUs": "Bizi değerlendirin", - "remindToEmptyDeviceTrash": "Ayrıca boşalan alanı talep etmek için \"Ayarlar\" -> \"Depolama\" bölümünden \"Son Silinenler \"i boşaltın", + "remindToEmptyDeviceTrash": "Ayrıca boş alanı kazanmak için \"Ayarlar\" > \"Depolama\" bölümünden \"Son Silinenler\" klasörünü de boşaltın", "youHaveSuccessfullyFreedUp": "Başarılı bir şekilde {storageSaved} alanını boşalttınız!", "@youHaveSuccessfullyFreedUp": { "description": "The text to display when the user has successfully freed up storage", @@ -543,15 +559,15 @@ } }, "familyPlans": "Aile Planı", - "referrals": "Referanslar", + "referrals": "Arkadaşını davet et", "notifications": "Bildirimler", "sharedPhotoNotifications": "Paylaşılan fotoğrafları ekle", - "sharedPhotoNotificationsExplanation": "Birisi sizin de parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın", + "sharedPhotoNotificationsExplanation": "Birisi parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın", "advanced": "Gelişmiş", "general": "Genel", "security": "Güvenlik", "authToViewYourRecoveryKey": "Kurtarma anahtarınızı görmek için lütfen kimliğinizi doğrulayın", - "twofactor": "İki faktör", + "twofactor": "İki faktörlü doğrulama", "authToConfigureTwofactorAuthentication": "İki faktörlü kimlik doğrulamayı yapılandırmak için lütfen kimlik doğrulaması yapın", "lockscreen": "Kilit ekranı", "authToChangeLockscreenSetting": "Kilit ekranı ayarını değiştirmek için lütfen kimliğinizi doğrulayın", @@ -571,7 +587,7 @@ "discord": "Discord", "reddit": "Reddit", "yourStorageDetailsCouldNotBeFetched": "Depolama bilgisi alınamadı", - "reportABug": "Hatayı bildir", + "reportABug": "Hata bildir", "reportBug": "Hata bildir", "suggestFeatures": "Özellik önerin", "support": "Destek", @@ -591,7 +607,7 @@ }, "type": "text" }, - "faqs": "Sık sorulanlar", + "faqs": "Sık Sorulan Sorular", "renewsOn": "Abonelik {endDate} tarihinde yenilenir", "freeTrialValidTill": "Ücretsiz deneme {endDate} sona erir", "validTill": "{endDate} tarihine kadar geçerli", @@ -640,13 +656,13 @@ "askCancelReason": "Aboneliğiniz iptal edilmiştir. Bunun sebebini paylaşmak ister misiniz?", "thankYouForSubscribing": "Abone olduğunuz için teşekkürler!", "yourPurchaseWasSuccessful": "Satın alım başarılı", - "yourPlanWasSuccessfullyUpgraded": "Planınız başarılı şekilde yükseltildi", + "yourPlanWasSuccessfullyUpgraded": "Planınız başarıyla yükseltildi", "yourPlanWasSuccessfullyDowngraded": "Planınız başarıyla düşürüldü", "yourSubscriptionWasUpdatedSuccessfully": "Aboneliğiniz başarıyla güncellendi", - "googlePlayId": "Google play kimliği", - "appleId": "Apple kimliği", + "googlePlayId": "Google Play ID", + "appleId": "Apple ID", "playstoreSubscription": "PlayStore aboneliği", - "appstoreSubscription": "PlayStore aboneliği", + "appstoreSubscription": "AppStore aboneliği", "subAlreadyLinkedErrMessage": "{id}'niz zaten başka bir ente hesabına bağlı.\n{id} numaranızı bu hesapla kullanmak istiyorsanız lütfen desteğimizle iletişime geçin''", "visitWebToManage": "Aboneliğinizi yönetmek için lütfen web.ente.io adresini ziyaret edin", "couldNotUpdateSubscription": "Abonelikler kaydedilemedi", @@ -664,7 +680,7 @@ } }, "continueOnFreeTrial": "Ücretsiz denemeye devam et", - "areYouSureYouWantToExit": "Çıkmak istediğinden emin misin?", + "areYouSureYouWantToExit": "Çıkmak istediğinden emin misiniz?", "thankYou": "Teşekkürler", "failedToVerifyPaymentStatus": "Ödeme durumu doğrulanamadı", "pleaseWaitForSometimeBeforeRetrying": "Tekrar denemeden önce lütfen bir süre bekleyin", @@ -673,8 +689,8 @@ "contactFamilyAdmin": "Aboneliğinizi yönetmek için lütfen {familyAdminEmail} ile iletişime geçin", "leaveFamily": "Aile planından ayrıl", "areYouSureThatYouWantToLeaveTheFamily": "Aile planından ayrılmak istediğinize emin misiniz?", - "leave": "Çıkış yap", - "rateTheApp": "Uygulamaya puan verin", + "leave": "Ayrıl", + "rateTheApp": "Uygulamayı puanlayın", "startBackup": "Yedeklemeyi başlat", "noPhotosAreBeingBackedUpRightNow": "Şu anda hiçbir fotoğraf yedeklenmiyor", "preserveMore": "Daha fazlasını koruyun", @@ -685,7 +701,7 @@ "selectMorePhotos": "Daha Fazla Fotoğraf Seç", "existingUser": "Mevcut kullanıcı", "privateBackups": "Özel yedeklemeler", - "forYourMemories": "anıların için", + "forYourMemories": "anılarınız için", "endtoendEncryptedByDefault": "Varsayılan olarak uçtan uca şifrelenmiş", "safelyStored": "Güvenle saklanır", "atAFalloutShelter": "serpinti sığınağında", @@ -706,6 +722,7 @@ "type": "text" }, "backupFailed": "Yedekleme başarısız oldu", + "sorryBackupFailedDesc": "Üzgünüz, bu dosya şu anda yedeklenemedi. Daha sonra tekrar deneyeceğiz.", "couldNotBackUpTryLater": "Verilerinizi yedekleyemedik.\nDaha sonra tekrar deneyeceğiz.", "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente dosyaları yalnızca erişim izni verdiğiniz takdirde şifreleyebilir ve koruyabilir", "pleaseGrantPermissions": "Lütfen izin ver", @@ -722,7 +739,7 @@ "description": "The text displayed above folders/albums stored on device", "type": "text" }, - "onDevice": "Bu cihaz", + "onDevice": "Cihazda", "@onEnte": { "description": "The text displayed above albums backed up to Ente", "type": "text" @@ -732,7 +749,7 @@ "newest": "En yeni", "lastUpdated": "En son güncellenen", "deleteEmptyAlbums": "Boş albümleri sil", - "deleteEmptyAlbumsWithQuestionMark": "Boş albümleri sileyim mi?", + "deleteEmptyAlbumsWithQuestionMark": "Boş albümler silinsin mi?", "deleteAlbumsDialogBody": "Bu, tüm boş albümleri silecektir. Bu, albüm listenizdeki dağınıklığı azaltmak istediğinizde kullanışlıdır.", "deleteProgress": "Siliniyor {currentlyDeleting} / {totalCount}", "genericProgress": "Siliniyor {currentlyProcessing} / {totalCount}", @@ -752,10 +769,10 @@ }, "permanentlyDelete": "Kalıcı olarak sil", "canOnlyCreateLinkForFilesOwnedByYou": "Yalnızca size ait dosyalar için bağlantı oluşturabilir", - "publicLinkCreated": "Herkese açık link oluşturuldu", + "publicLinkCreated": "Herkese açık bağlantı oluşturuldu", "youCanManageYourLinksInTheShareTab": "Bağlantılarınızı paylaşım sekmesinden yönetebilirsiniz.", "linkCopiedToClipboard": "Link panoya kopyalandı", - "restore": "Yenile", + "restore": "Geri yükle", "@restore": { "description": "Display text for an action which triggers a restore of item from trash", "type": "text" @@ -765,7 +782,7 @@ "unarchive": "Arşivden cıkar", "favorite": "Favori", "removeFromFavorite": "Favorilerden Kaldır", - "shareLink": "Linki paylaş", + "shareLink": "Bağlantıyı paylaş", "createCollage": "Kolaj oluştur", "saveCollage": "Kolajı kaydet", "collageSaved": "Kolajınız galeriye kaydedildi", @@ -777,6 +794,14 @@ "share": "Paylaş", "unhideToAlbum": "Albümü gizleme", "restoreToAlbum": "Albümü yenile", + "moveItem": "{count, plural,=1 {Öğeyi taşı} other {Öğeleri taşı}}", + "@moveItem": { + "description": "Page title while moving one or more items to an album" + }, + "addItem": "{count, plural,=1 {Öğe ekle} other {Öğeler ekle}}", + "@addItem": { + "description": "Page title while adding one or more items to album" + }, "createOrSelectAlbum": "Albüm oluştur veya seç", "selectAlbum": "Albüm seçin", "searchByAlbumNameHint": "Albüm adı", @@ -800,8 +825,8 @@ "doubleYourStorage": "Depolama alanınızı ikiye katlayın", "referFriendsAnd2xYourPlan": "Arkadaşlarınıza önerin ve planınızı 2 katına çıkarın", "shareAlbumHint": "Bir albüm açın ve paylaşmak için sağ üstteki paylaş düğmesine dokunun.", - "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Öğeler, kalıcı olarak silinmeden önce kalan gün sayısını gösterir", - "trashDaysLeft": "{count, plural, =0 {yakında} =1{1 gün} other {{count} gün}}", + "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Öğeler kalıcı olarak silinmeden önce kalan gün sayısını gösterir", + "trashDaysLeft": "{count, plural, =0 {Yakında} =1{1 gün} other {{count} gün}}", "@trashDaysLeft": { "description": "Text to indicate number of days remaining before permanent deletion", "placeholders": { @@ -825,7 +850,7 @@ "leaveSharedAlbum": "Paylaşılan albüm silinsin mi?", "leaveAlbum": "Albümü yeniden adlandır", "photosAddedByYouWillBeRemovedFromTheAlbum": "Eklediğiniz fotoğraflar albümden kaldırılacak", - "youveNoFilesInThisAlbumThatCanBeDeleted": "Bu cihazda silinebilecek hiçbir dosyanız yok", + "youveNoFilesInThisAlbumThatCanBeDeleted": "Her şey zaten temiz, silinecek dosya kalmadı", "youDontHaveAnyArchivedItems": "Arşivlenmiş öğeniz yok.", "ignoredFolderUploadReason": "Bu albümdeki bazı dosyalar daha önce ente'den silindiğinden yükleme işleminde göz ardı edildi.", "resetIgnoredFiles": "Yok sayılan dosyaları sıfırla", @@ -874,6 +899,7 @@ "authToViewYourMemories": "Kodlarınızı görmek için lütfen kimlik doğrulaması yapın", "unlock": "Kilidi aç", "freeUpSpace": "Boş alan", + "freeUpSpaceSaving": "{count, plural, =1 {Cihazdan silinerek {formattedSize} boşaltılabilir} other {Cihazdan silinerek {formattedSize} boşaltılabilirler}}", "filesBackedUpInAlbum": "Bu albümdeki {count, plural, one {1 file} other {{formattedNumber} dosya}} güvenli bir şekilde yedeklendi", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", @@ -904,6 +930,18 @@ } } }, + "@freeUpSpaceSaving": { + "description": "Text to tell user how much space they can free up by deleting items from the device" + }, + "freeUpAccessPostDelete": "Aktif bir aboneliğiniz olduğu sürece Ente üzerinden {count, plural, =1 {ona} other {onlara}} hâlâ erişebilirsiniz", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "{sizeInMBorGB} yer açın", "thisEmailIsAlreadyInUse": "Bu e-posta zaten kullanılıyor", "incorrectCode": "Yanlış kod", @@ -973,19 +1011,19 @@ "tempErrorContactSupportIfPersists": "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin.", "networkHostLookUpErr": "Ente'ye bağlanılamıyor. Lütfen ağ ayarlarınızı kontrol edin ve hata devam ederse destek ekibiyle iletişime geçin.", "networkConnectionRefusedErr": "Ente'ye bağlanılamıyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse lütfen desteğe başvurun.", - "cachedData": "Ön belleğe alınan veri", - "clearCaches": "Önbellekleri temizle", - "remoteImages": "Uzaktan Görüntüler", - "remoteVideos": "Uzak videolar", - "remoteThumbnails": "Uzak Küçük Resim", - "pendingSync": "Senkronizasyon bekleniyor", + "cachedData": "Önbelleğe alınmış veriler", + "clearCaches": "Önbelleği temizle", + "remoteImages": "Uzak Görseller", + "remoteVideos": "Uzak Videolar", + "remoteThumbnails": "Uzak Küçük Resimler", + "pendingSync": "Bekleyen Senkronizasyonlar", "localGallery": "Yerel galeri", - "todaysLogs": "Bugünün günlükleri", - "viewLogs": "Günlükleri göster", - "logsDialogBody": "Bu, sorununuzu gidermemize yardımcı olmak için günlükleri gönderecektir. Belirli dosyalarla ilgili sorunların izlenmesine yardımcı olmak için dosya adlarının ekleneceğini lütfen unutmayın.", - "preparingLogs": "Günlük hazırlanıyor...", - "emailYourLogs": "Günlüklerinizi e-postayla gönderin", - "pleaseSendTheLogsTo": "Lütfen günlükleri şu adrese gönderin\n{toEmail}", + "todaysLogs": "Bugünün kayıtları", + "viewLogs": "Kayıtları görüntüle", + "logsDialogBody": "Bu, sorununuzu gidermemize yardımcı olmak için kayıtları gönderecektir. Belirli dosyalarla ilgili sorunların izlenmesine yardımcı olmak için dosya adlarının ekleneceğini lütfen unutmayın.", + "preparingLogs": "Kayıtlar hazırlanıyor...", + "emailYourLogs": "Kayıtlarınızı e-postayla gönderin", + "pleaseSendTheLogsTo": "Lütfen kayıtları şu adrese gönderin\n{toEmail}", "copyEmailAddress": "E-posta adresini kopyala", "exportLogs": "Günlüğü dışa aktar", "pleaseEmailUsAt": "Lütfen bize {toEmail} adresinden ulaşın", @@ -993,7 +1031,7 @@ "didYouKnow": "Biliyor musun?", "loadingMessage": "Fotoğraflarınız yükleniyor...", "loadMessage1": "Aboneliğinizi ailenizle paylaşabilirsiniz", - "loadMessage2": "Şu ana kadar 30 milyondan fazla anıyı koruduk", + "loadMessage2": "Şimdiye kadar 200 milyondan fazla anıyı koruduk", "loadMessage3": "Verilerinizin 3 kopyasını saklıyoruz, biri yer altı serpinti sığınağında", "loadMessage4": "Tüm uygulamalarımız açık kaynaktır", "loadMessage5": "Kaynak kodumuz ve şifrelememiz harici olarak denetlenmiştir", @@ -1005,7 +1043,7 @@ "fileTypesAndNames": "Dosya türleri ve adları", "location": "Konum", "moments": "Anlar", - "searchFaceEmptySection": "İndeksleme yapıldıktan sonra insanlar burada gösterilecek", + "searchFaceEmptySection": "Dizinleme yapıldıktan sonra insanlar burada gösterilecek", "searchDatesEmptySection": "Tarihe, aya veya yıla göre arama yapın", "searchLocationEmptySection": "Bir fotoğrafın belli bir yarıçapında çekilen fotoğrafları gruplandırın", "searchPeopleEmptySection": "İnsanları davet ettiğinizde onların paylaştığı tüm fotoğrafları burada göreceksiniz", @@ -1124,7 +1162,7 @@ "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, - "openstreetmapContributors": "© OpenStreetMap katkıda bululanlar", + "openstreetmapContributors": "OpenStreetMap katkıda bululanlar", "hostedAtOsmFrance": "OSM Fransa'da ağırlandı", "map": "Harita", "@map": { @@ -1161,7 +1199,7 @@ "moveToHiddenAlbum": "Gizli albüme ekle", "fileTypes": "Dosya türü", "deleteConfirmDialogBody": "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır. Tüm Ente uygulamalarına yüklediğiniz veriler ve hesabınız kalıcı olarak silinecektir.", - "hearUsWhereTitle": "Ente'yi nereden duydunuz? (opsiyonel)", + "hearUsWhereTitle": "Ente'yi nereden duydunuz? (isteğe bağlı)", "hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!", "viewAddOnButton": "Eklentileri görüntüle", "addOns": "Eklentiler", @@ -1211,7 +1249,7 @@ "cleanUncategorized": "Temiz Genel", "cleanUncategorizedDescription": "Diğer albümlerde bulunan Kategorilenmemiş tüm dosyaları kaldırın", "waitingForVerification": "Doğrulama bekleniyor...", - "passkey": "Parola Anahtarı", + "passkey": "Geçiş anahtarı", "passkeyAuthTitle": "Geçiş anahtarı doğrulaması", "loginWithTOTP": "TOTP ile giriş yap", "passKeyPendingVerification": "Doğrulama hala bekliyor", @@ -1226,11 +1264,13 @@ "joinDiscord": "Discord'a Katıl", "locations": "Konum", "addAName": "Bir Ad Ekle", - "findThemQuickly": "Onları çabucak bulun", + "findThemQuickly": "Çabucak bulun", "@findThemQuickly": { "description": "Subtitle to indicate that the user can find people quickly by name" }, - "findPeopleByName": "Kişileri isimlere göre çabucak bulun", + "findPeopleByName": "Kişileri isimlerine göre bulun", + "addViewers": "{count, plural, =0 {Görüntüleyen ekle} =1 {Görüntüleyen ekle} other {Görüntüleyen ekle}}", + "addCollaborators": "{count, plural, =0 {Ortak çalışan ekle} =1 {Ortak çalışan ekle} other {Ortak çalışan ekle}}", "longPressAnEmailToVerifyEndToEndEncryption": "Uçtan uca şifrelemeyi doğrulamak için bir e-postaya uzun basın.", "developerSettingsWarning": "Geliştirici ayarlarını değiştirmek istediğinizden emin misiniz?", "developerSettings": "Geliştirici ayarları", @@ -1242,9 +1282,11 @@ "createCollaborativeLink": "Ortak bağlantı oluşturun", "search": "Ara", "enterPersonName": "Kişi ismini giriniz", + "editEmailAlreadyLinked": "Bu e-posta zaten {name} kişisine bağlı.", + "viewPersonToUnlink": "Bağlantıyı kaldırmak için {name} kişisini görüntüle", "enterName": "İsim girin", - "savePerson": "Kişiyi Kaydet", - "editPerson": "Kişiyi Düzenle", + "savePerson": "Kişiyi kaydet", + "editPerson": "Kişiyi düzenle", "mergedPhotos": "Birleştirilmiş fotoğraflar", "orMergeWithExistingPerson": "Ya da mevcut olan ile birleştirin", "enterDateOfBirth": "Doğum Günü (isteğe bağlı)", @@ -1267,7 +1309,7 @@ "faceRecognition": "Yüz Tanıma", "foundFaces": "Yüzler bulundu", "clusteringProgress": "Kümeleme ilerlemesi", - "indexingIsPaused": "İndeksleme duraklatılmıştır. Cihaz hazır olduğunda otomatik olarak devam edecektir.", + "indexingIsPaused": "Dizin oluşturma duraklatıldı. Cihaz hazır olduğunda otomatik olarak devam edecektir.", "trim": "Kes", "crop": "Kırp", "rotate": "Döndür", @@ -1290,7 +1332,7 @@ "enable": "Etkinleştir", "enabled": "Etkin", "moreDetails": "Daha fazla detay", - "enableMLIndexingDesc": "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde makine öğrenimini destekler", + "enableMLIndexingDesc": "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde çalışan makine öğrenimini kullanır", "magicSearchHint": "Sihirli arama, fotoğrafları içeriklerine göre aramanıza olanak tanır, örneğin 'çiçek', 'kırmızı araba', 'kimlik belgeleri'", "panorama": "Panorama", "reenterPassword": "Şifrenizi tekrar girin", @@ -1308,14 +1350,14 @@ "videoInfo": "Video Bilgileri", "autoLock": "Otomatik Kilit", "immediately": "Hemen", - "autoLockFeatureDescription": "Uygulamayı arka plana attıktan sonra kilitlendiği süre", + "autoLockFeatureDescription": "Uygulama arka plana geçtikten sonra kilitleneceği süre", "hideContent": "İçeriği gizle", "hideContentDescriptionAndroid": "Uygulama değiştiricide bulunan uygulama içeriğini gizler ve ekran görüntülerini devre dışı bırakır", "hideContentDescriptionIos": "Uygulama değiştiricideki uygulama içeriğini gizler", "passwordStrengthInfo": "Parola gücü, parolanın uzunluğu, kullanılan karakterler ve parolanın en çok kullanılan ilk 10.000 parola arasında yer alıp almadığı dikkate alınarak hesaplanır", "noQuickLinksSelected": "Hızlı bağlantılar seçilmedi", "pleaseSelectQuickLinksToRemove": "Lütfen kaldırmak için hızlı bağlantıları seçin", - "removePublicLinks": "Herkese açık link oluştur", + "removePublicLinks": "Herkese açık bağlantıları kaldır", "thisWillRemovePublicLinksOfAllSelectedQuickLinks": "Bu, seçilen tüm hızlı bağlantıların genel bağlantılarını kaldıracaktır.", "guestView": "Misafir Görünümü", "guestViewEnablePreSteps": "Misafir görünümünü etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın.", @@ -1351,17 +1393,27 @@ }, "extraPhotosFound": "Ekstra fotoğraflar bulundu", "configuration": "Yapılandırma", - "localIndexing": "Yerel indeksleme", + "localIndexing": "Yerel dizinleme", "processed": "İşlenen", "resetPerson": "Kaldır", - "areYouSureYouWantToResetThisPerson": "Bu kişiyi sıfırlamak istediğinden emin misin?", + "areYouSureYouWantToResetThisPerson": "Bu kişiyi sıfırlamak istediğinden emin misiniz?", "allPersonGroupingWillReset": "Bu kişi için tüm gruplamalar sıfırlanacak ve bu kişi için yaptığınız tüm önerileri kaybedeceksiniz", "yesResetPerson": "Evet, kişiyi sıfırla", "onlyThem": "Sadece onlar", - "checkingModels": "Modelleri kontrol ediyorum...", + "checkingModels": "Modeller kontrol ediliyor...", "enableMachineLearningBanner": "Sihirli arama ve yüz tanıma için makine öğrenimini etkinleştirin", "searchDiscoverEmptySection": "İşleme ve senkronizasyon tamamlandığında görüntüler burada gösterilecektir", "searchPersonsEmptySection": "İşleme ve senkronizasyon tamamlandığında kişiler burada gösterilecektir", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 izleyici eklendi} =1 {1 izleyici eklendi} other {{count} izleyici eklendi}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 işbirlikçi eklendi} =1 {1 işbirlikçi eklendi} other {{count} işbirlikçi eklendi}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { @@ -1437,6 +1489,15 @@ }, "currentlyRunning": "şu anda çalışıyor", "ignored": "yoksayıldı", + "photosCount": "{count, plural, =0 {0 fotoğraf} =1 {1 fotoğraf} other {{count} fotoğraf}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, "file": "Dosya", "searchSectionsLengthMismatch": "Bölüm uzunluğu uyuşmazlığı: {snapshotLength} != {searchLength}", "@searchSectionsLengthMismatch": { @@ -1557,9 +1618,9 @@ } } }, - "reassignMe": "\"Ben \"i yeniden atayın", + "reassignMe": "\"Ben\"i yeniden atayın", "me": "Ben", - "linkEmailToContactBannerCaption": "d", + "linkEmailToContactBannerCaption": "daha hızlı paylaşım için", "@linkEmailToContactBannerCaption": { "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, @@ -1585,8 +1646,8 @@ } }, "selectYourFace": "Yüzünüzü seçin", - "reassigningLoading": "Yeniden atama...", - "reassignedToName": "Sizi {name}'e yeniden atadım", + "reassigningLoading": "Yeniden atanıyor...", + "reassignedToName": "Sizi {name}'e yeniden atadı", "@reassignedToName": { "placeholders": { "name": { @@ -1594,17 +1655,17 @@ } } }, - "saveChangesBeforeLeavingQuestion": "Ayrılmadan önce değişiklikleri kaydedin mi?", + "saveChangesBeforeLeavingQuestion": "Çıkmadan önce değişiklikler kaydedilsin mi?", "dontSave": "Kaydetme", "thisIsMeExclamation": "Bu benim!", - "linkPerson": "Bağlantı kişisi", + "linkPerson": "Kişiyi bağla", "linkPersonCaption": "daha iyi paylaşım deneyimi için", "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "Video akışı", + "videoStreaming": "Akışlandırılabilir videolar", "processingVideos": "Videolar işleniyor", - "streamDetails": "Yayın detayları", + "streamDetails": "Akış detayları", "processing": "İşleniyor", "queued": "Kuyrukta", "ineligible": "Uygun Değil", @@ -1637,17 +1698,42 @@ }, "appIcon": "Uygulama simgesi", "notThisPerson": "Bu kişi değil mi?", - "selectedItemsWillBeRemovedFromThisPerson": "Seçilen öğeler bu kişiden kaldırılacak, ancak kitaplığınızdan silinmeyecektir.", + "selectedItemsWillBeRemovedFromThisPerson": "Seçili öğeler bu kişiden silinir, ancak kitaplığınızdan silinmez.", "throughTheYears": "{dateFormat} yıllar boyunca", "thisWeekThroughTheYears": "Yıllar boyunca bu hafta", - "cLIcon": "Yeni Simge", - "cLIconDesc": "Son olarak, çalışmalarımızı en iyi şekilde temsil ettiğini düşündüğümüz yeni bir uygulama simgesi. Eski simgeyi kullanmaya devam edebilmeniz için bir simge değiştirici de ekledik.", - "cLMemories": "Anılar", - "cLMemoriesDesc": "Özel anlarınızı yeniden keşfedin - en sevdiğiniz kişilere, seyahatlerinize ve tatillerinize, en iyi tıklamalarınıza ve çok daha fazlasına odaklanın. En iyi deneyim için makine öğrenimini açın, kendinizi etiketleyin ve arkadaşlarınızı adlandırın.", - "cLWidgets": "Widget'lar", - "cLWidgetsDesc": "Anılarla entegre edilmiş ana ekran widget'ları artık kullanılabilir. Uygulamayı açmadan özel anlarınızı gösterecekler.", - "cLFamilyPlan": "Aile Planı Sınırları", - "cLFamilyPlanDesc": "Artık aile üyelerinizin ne kadar depolama alanı kullanabileceğine dair sınırlar belirleyebilirsiniz.", - "cLBulkEditDesc": "Artık birden fazla fotoğraf seçebilir ve tek bir hızlı işlemle hepsi için tarih/saat düzenleyebilirsiniz. Tarih kaydırma da desteklenmektedir.", - "curatedMemories": "Seçilmiş anılar" + "thisWeekXYearsAgo": "{count, plural, =1 {Bu hafta, {count} yıl önce} other {Bu hafta, {count} yıl önce}}", + "youAndThem": "Sen ve {name}", + "admiringThem": "{name}'e hayran kalmak", + "embracingThem": "{name}'e sarılmak", + "partyWithThem": "{name} ile parti", + "hikingWithThem": "{name} ile doğa yürüyüşü", + "feastingWithThem": "{name} ile ziyafet", + "selfiesWithThem": "{name} ile selfieler", + "posingWithThem": "{name} ile poz verme", + "backgroundWithThem": "{name} ile güzel manzaralar", + "sportsWithThem": "{name} ile spor", + "roadtripWithThem": "{name} ile yolculuk", + "spotlightOnYourself": "Sahne senin", + "spotlightOnThem": "Sahne {name}'in", + "personIsAge": "{name} {age} yaşında!", + "personTurningAge": "{name} yakında {age} yaşına giriyor", + "lastTimeWithThem": "{name} ile son an", + "tripToLocation": "{location}'a gezi", + "tripInYear": "{year} yılındaki gezi", + "lastYearsTrip": "Geçen yılki gezi", + "sunrise": "Ufukta", + "mountains": "Tepelerin ötesinde", + "greenery": "Yeşil yaşam", + "beach": "Kum ve deniz", + "city": "Şehirde", + "moon": "Ay ışığında", + "onTheRoad": "Yeniden yollarda", + "food": "Yemek keyfi", + "pets": "Tüylü dostlar", + "curatedMemories": "Seçilmiş anılar", + "deleteMultipleAlbumDialog": "Ayrıca bu {count} albümde bulunan fotoğrafları (ve videoları) parçası oldukları tüm diğer albümlerden silmek istiyor musunuz?", + "addParticipants": "Katılımcı ekle", + "selectedAlbums": "{count} seçildi", + "actionNotSupportedOnFavouritesAlbum": "Favoriler albümünde eylem desteklenmiyor", + "onThisDay": "Bu günde" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_uk.arb b/mobile/lib/l10n/intl_uk.arb index 31b446eac3..c13e19312c 100644 --- a/mobile/lib/l10n/intl_uk.arb +++ b/mobile/lib/l10n/intl_uk.arb @@ -439,7 +439,7 @@ "skip": "Пропустити", "updatingFolderSelection": "Оновлення вибору теки...", "itemCount": "{count, plural, one{{count} елемент} few {{count} елементи} many {{count} елементів} other{{count} елементів}}", - "deleteItemCount": "{count, plural, =1 {Видалено {count} елемент} few {Видалено {count} елементи} many {Видалено {count} елементів} other {Видалено {count} елементів}}", + "deleteItemCount": "{count, plural, =1 {Видалено {count} елемент} other {Видалено {count} елементів}}", "duplicateItemsGroup": "{count} файлів, кожен по {formattedSize}", "@duplicateItemsGroup": { "description": "Display the number of duplicate files and their size", @@ -456,7 +456,7 @@ } }, "showMemories": "Показати спогади", - "yearsAgo": "{count, plural, one{{count} рік тому} few {{count} роки тому} many {{count} років тому} other{{count} років тому}}", + "yearsAgo": "{count, plural, one{{count} рік тому} other{{count} років тому}}", "backupSettings": "Налаштування резервного копіювання", "backupStatus": "Стан резервного копіювання", "backupStatusDescription": "Елементи, для яких було створено резервну копію, показуватимуться тут", @@ -868,7 +868,7 @@ "authToViewYourMemories": "Авторизуйтеся, щоб переглянути ваші спогади", "unlock": "Розблокувати", "freeUpSpace": "Звільнити місце", - "filesBackedUpInAlbum": "{count, plural, one {Для 1 файлу} few {Для {formattedNumber} файлів} many {Для {formattedNumber} файлів} other {Для {formattedNumber} файлів}} у цьому альбомі було створено резервну копію", + "filesBackedUpInAlbum": "{count, plural, one {Для 1 файлу} other {Для {formattedNumber} файлів}} у цьому альбомі було створено резервну копію", "@filesBackedUpInAlbum": { "description": "Text to tell user how many files have been backed up in the album", "placeholders": { @@ -987,7 +987,6 @@ "didYouKnow": "Чи знали ви?", "loadingMessage": "Завантажуємо ваші фотографії...", "loadMessage1": "Ви можете поділитися своєю передплатою з родиною", - "loadMessage2": "На цей час ми зберегли понад 30 мільйонів спогадів", "loadMessage3": "Ми зберігаємо 3 копії ваших даних, одну в підземному бункері", "loadMessage4": "Всі наші застосунки мають відкритий код", "loadMessage5": "Наш вихідний код та шифрування пройшли перевірку спільнотою", @@ -1355,16 +1354,6 @@ "enableMachineLearningBanner": "Увімкніть машинне навчання для магічного пошуку та розпізнавання облич", "searchDiscoverEmptySection": "Зображення будуть показані тут після завершення оброблення та синхронізації", "searchPersonsEmptySection": "Люди будуть показані тут після завершення оброблення та синхронізації", - "collaboratorsSuccessfullyAdded": "{count, plural, one {} few {Додано {count} співаторів} many {Додано {count} співаторів}=0 {Додано 0 співавторів} =1 {Додано 1 співавтор} other {Додано {count} співавторів}}", - "@collaboratorsSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of collaborators that were successfully added to an album." - }, "accountIsAlreadyConfigured": "Обліковий запис уже налаштовано.", "sessionIdMismatch": "Невідповідність ідентифікатора сеансу", "@sessionIdMismatch": { diff --git a/mobile/lib/l10n/intl_vi.arb b/mobile/lib/l10n/intl_vi.arb index dd56ae2ae6..9943afc24a 100644 --- a/mobile/lib/l10n/intl_vi.arb +++ b/mobile/lib/l10n/intl_vi.arb @@ -992,7 +992,6 @@ "didYouKnow": "Bạn có biết?", "loadingMessage": "Đang tải ảnh của bạn...", "loadMessage1": "Bạn có thể chia sẻ đăng ký của mình với gia đình", - "loadMessage2": "Chúng tôi đã bảo tồn hơn 30 triệu kỷ niệm cho đến nay", "loadMessage3": "Chúng tôi giữ 3 bản sao dữ liệu của bạn, một trong nơi trú ẩn dưới lòng đất", "loadMessage4": "Tất cả các ứng dụng của chúng tôi đều là mã nguồn mở", "loadMessage5": "Mã nguồn và mật mã của chúng tôi đã được kiểm toán bên ngoài", diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index b8ccddf71f..650f26d333 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -1029,7 +1029,6 @@ "didYouKnow": "您知道吗?", "loadingMessage": "正在加载您的照片...", "loadMessage1": "您可以与家庭分享您的订阅", - "loadMessage2": "到目前为止,我们已经保存了超过3 000万个回忆", "loadMessage3": "我们保存你的3个数据副本,其中一个在地下安全屋中", "loadMessage4": "我们所有的应用程序都是开源的", "loadMessage5": "我们的源代码和加密技术已经由外部审计", @@ -1659,7 +1658,6 @@ "@linkPersonCaption": { "description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." }, - "videoStreaming": "影音流", "processingVideos": "正在处理视频", "streamDetails": "流详情", "processing": "正在处理", @@ -1725,15 +1723,5 @@ "moon": "月光之下", "onTheRoad": "再次踏上旅途", "food": "美食盛宴", - "pets": "毛茸茸的伙伴", - "cLIcon": "新图标", - "cLIconDesc": "终于迎来了一个全新的应用图标,我们认为它最能代表我们的作品。同时,我们还添加了图标切换功能,所以您可以继续使用旧图标。", - "cLMemories": "回忆", - "cLMemoriesDesc": "重新发现你的珍贵时刻——聚焦你最爱的亲友、旅行与假期、美妙瞬间等精彩回忆。启用机器学习,标记自己并为朋友命名,享受最佳体验。", - "cLWidgets": "小组件", - "cLWidgetsDesc": "全新首页小组件,与回忆深度集成。无需打开应用,即可在主屏幕上查看你的特别时刻。", - "cLFamilyPlan": "家庭计划存储限制", - "cLFamilyPlanDesc": "你现在可以为家庭成员设置存储空间使用上限。", - "cLBulkEdit": "批量编辑日期", - "cLBulkEditDesc": "你现在可以选择多张照片,一键批量修改日期/时间,并支持日期顺移。" + "pets": "毛茸茸的伙伴" } \ No newline at end of file diff --git a/mobile/lib/l10n/l10n.dart b/mobile/lib/l10n/l10n.dart index 5154f8840c..4ed4ff7fe3 100644 --- a/mobile/lib/l10n/l10n.dart +++ b/mobile/lib/l10n/l10n.dart @@ -29,21 +29,51 @@ const List appSupportedLocales = [ Locale("zh", "CN"), ]; +List _onDeviceLocales = []; Locale? autoDetectedLocale; -Locale localResolutionCallBack(locales, supportedLocales) { - for (Locale locale in locales) { - for (Locale supportedLocale in appSupportedLocales) { - if (supportedLocale == locale) { - autoDetectedLocale = supportedLocale; - return supportedLocale; - } else if (supportedLocale.languageCode == locale.languageCode) { - autoDetectedLocale = supportedLocale; - return supportedLocale; - } +Locale localResolutionCallBack(deviceLocales, supportedLocales) { + _onDeviceLocales = deviceLocales; + final Set languageSupport = {}; + for (Locale supportedLocale in appSupportedLocales) { + languageSupport.add(supportedLocale.languageCode); + } + for (Locale locale in deviceLocales) { + // check if exact local is supported, if yes, return it + if (appSupportedLocales.contains(locale)) { + autoDetectedLocale = locale; + return locale; + } + // check if language code is supported, if yes, return it + if (languageSupport.contains(locale.languageCode)) { + autoDetectedLocale = locale; + return locale; } } - return const Locale('en'); + // Return the first language code match or default to 'en' + return autoDetectedLocale ?? const Locale('en'); +} + +// This is used to get locale that should be used for various formatting +// operations like date, time, number etc. For common languages like english, different +// locale might have different formats. For example, en_US and en_GB have different +// formats for date and time. Use this method to find the best locale for formatting +// operations. This is not used for displaying text in the app. +Future getFormatLocale() async { + final Locale locale = (await getLocale())!; + Locale? firstLanguageMatch; + // see if exact matche is present in the device locales + for (Locale deviceLocale in _onDeviceLocales) { + if (deviceLocale.languageCode == locale.languageCode && + deviceLocale.countryCode == locale.countryCode) { + return deviceLocale; + } + if (firstLanguageMatch == null && + deviceLocale.languageCode == locale.languageCode) { + firstLanguageMatch = deviceLocale; + } + } + return firstLanguageMatch ?? locale; } Future getLocale({ diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c4ae34fa28..b8e72c0dd0 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -31,19 +31,18 @@ import "package:photos/services/account/user_service.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; -import "package:photos/services/filedata/filedata_service.dart"; import 'package:photos/services/home_widget_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/services/machine_learning/ml_service.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import "package:photos/services/notification_service.dart"; -import "package:photos/services/preview_video_store.dart"; import 'package:photos/services/push_service.dart'; import 'package:photos/services/search_service.dart'; import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/services/sync/remote_sync_service.dart'; import "package:photos/services/sync/sync_service.dart"; +import "package:photos/services/video_preview_service.dart"; import "package:photos/services/wake_lock_service.dart"; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/ui/tools/lock_screen.dart'; @@ -223,8 +222,12 @@ Future _init(bool isBackground, {String via = ''}) async { await NetworkClient.instance.init(packageInfo); _logger.info("NetworkClient init done $tlog"); - ServiceLocator.instance - .init(preferences, NetworkClient.instance.enteDio, packageInfo); + ServiceLocator.instance.init( + preferences, + NetworkClient.instance.enteDio, + NetworkClient.instance.getDio(), + packageInfo, + ); _logger.info("UserService init $tlog"); await UserService.instance.init(); @@ -237,7 +240,6 @@ Future _init(bool isBackground, {String via = ''}) async { FavoritesService.instance.initFav().ignore(); LocalFileUpdateService.instance.init(preferences); SearchService.instance.init(); - FileDataService.instance.init(preferences); _logger.info("FileUploader init $tlog"); await FileUploader.instance.init(preferences, isBackground); @@ -254,7 +256,7 @@ Future _init(bool isBackground, {String via = ''}) async { await SyncService.instance.init(preferences); _logger.info("SyncService init done $tlog"); - await HomeWidgetService.instance.init(preferences); + HomeWidgetService.instance.init(preferences); if (!isBackground) { await _scheduleFGHomeWidgetSync(); @@ -269,7 +271,7 @@ Future _init(bool isBackground, {String via = ''}) async { }); } _logger.info("PushService/HomeWidget done $tlog"); - PreviewVideoStore.instance.init(preferences); + VideoPreviewService.instance.init(preferences); unawaited(SemanticSearchService.instance.init()); unawaited(MLService.instance.init()); await PersonService.init( @@ -300,7 +302,7 @@ void logLocalSettings() { ); _logger.info("Gallery grid size: ${localSettings.getPhotoGridSize()}"); _logger.info( - "Video streaming is enalbed: ${PreviewVideoStore.instance.isVideoStreamingEnabled}", + "Video streaming is enalbed: ${VideoPreviewService.instance.isVideoStreamingEnabled}", ); } diff --git a/mobile/lib/models/base/id.dart b/mobile/lib/models/base/id.dart index 2b095e0274..e275d298ad 100644 --- a/mobile/lib/models/base/id.dart +++ b/mobile/lib/models/base/id.dart @@ -2,16 +2,16 @@ import 'package:nanoid/nanoid.dart'; const enteWhiteListedAlphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; -const clusterIDLength = 22; +const randomIDLength = 22; String newClusterID() { - return "cluster_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "cluster_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } String newID(String prefix) { - return "${prefix}_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "${prefix}_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } String newIsolateTaskID(String task) { - return "${task}_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; + return "${task}_${customAlphabet(enteWhiteListedAlphabet, randomIDLength)}"; } diff --git a/mobile/lib/models/base_location.dart b/mobile/lib/models/base_location.dart index 7bdbb7eaec..d98152dad3 100644 --- a/mobile/lib/models/base_location.dart +++ b/mobile/lib/models/base_location.dart @@ -1,19 +1,18 @@ import "dart:convert"; -import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; const baseRadius = 0.6; class BaseLocation { - final List files; + final List fileIDs; int? firstCreationTime; int? lastCreationTime; final Location location; final bool isCurrentBase; BaseLocation( - this.files, + this.fileIDs, this.location, this.isCurrentBase, { this.firstCreationTime, @@ -22,12 +21,9 @@ class BaseLocation { static List decodeJsonToList( String jsonString, - Map filesMap, ) { final jsonList = jsonDecode(jsonString) as List; - return jsonList - .map((json) => BaseLocation.fromJson(json, filesMap)) - .toList(); + return jsonList.map((json) => BaseLocation.fromJson(json)).toList(); } static String encodeListToJson(List baseLocations) { @@ -38,13 +34,9 @@ class BaseLocation { static BaseLocation fromJson( Map json, - Map filesMap, ) { return BaseLocation( - (json['fileIDs'] as List) - .where((uploadID) => filesMap[uploadID] != null) - .map((uploadID) => filesMap[uploadID]!) - .toList(), + (json['fileIDs'] as List).cast(), Location( latitude: json['location']['latitude'], longitude: json['location']['longitude'], @@ -57,10 +49,7 @@ class BaseLocation { Map toJson() { return { - 'fileIDs': files - .where((file) => file.uploadedFileID != null) - .map((file) => file.uploadedFileID!) - .toList(), + 'fileIDs': fileIDs, 'location': { 'latitude': location.latitude!, 'longitude': location.longitude!, @@ -71,32 +60,15 @@ class BaseLocation { }; } - int averageCreationTime() { - if (firstCreationTime != null && lastCreationTime != null) { - return (firstCreationTime! + lastCreationTime!) ~/ 2; - } - final List creationTimes = files - .where((file) => file.creationTime != null) - .map((file) => file.creationTime!) - .toList(); - if (creationTimes.length < 2) { - return creationTimes.isEmpty ? 0 : creationTimes.first; - } - creationTimes.sort(); - firstCreationTime ??= creationTimes.first; - lastCreationTime ??= creationTimes.last; - return (firstCreationTime! + lastCreationTime!) ~/ 2; - } - BaseLocation copyWith({ - List? files, + List? files, int? firstCreationTime, int? lastCreationTime, Location? location, bool? isCurrentBase, }) { return BaseLocation( - files ?? this.files, + files ?? fileIDs, location ?? this.location, isCurrentBase ?? this.isCurrentBase, firstCreationTime: firstCreationTime ?? this.firstCreationTime, diff --git a/mobile/lib/models/collection/collection.dart b/mobile/lib/models/collection/collection.dart index fc00570e68..e51d3972bf 100644 --- a/mobile/lib/models/collection/collection.dart +++ b/mobile/lib/models/collection/collection.dart @@ -1,6 +1,8 @@ import 'dart:core'; import 'package:flutter/foundation.dart'; +import "package:photos/core/configuration.dart"; +import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/public_url.dart"; import "package:photos/models/api/collection/user.dart"; import "package:photos/models/metadata/collection_magic.dart"; @@ -61,7 +63,14 @@ class Collection { set sharedMagicMetadata(ShareeMagicMetadata? val) => _sharedMmd = val; // ignore: deprecated_member_use_from_same_package - String get displayName => decryptedName ?? name ?? "Unnamed Album"; + String get displayName { + if (!isDeleted && + type == CollectionType.favorites && + !isOwner(Configuration.instance.getUserID() ?? -1)) { + return '${owner.nameOrEmail}\'s favorites'; + } + return decryptedName ?? name ?? "Unnamed Album"; + } // set the value for both name and decryptedName till we finish migration void setName(String newName) { diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index ca0ef1031d..8f55363277 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -4,12 +4,11 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/file_magic.dart"; -import "package:photos/service_locator.dart"; +import "package:photos/module/download/file_url.dart"; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/panorama_util.dart"; @@ -242,47 +241,8 @@ class EnteFile { return metadata; } - String get downloadUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + "/files/download/" + uploadedFileID.toString(); - } else { - return "https://files.ente.io/?fileID=" + uploadedFileID.toString(); - } - } - - String get publicDownloadUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + - "/public-collection/files/download/" + - uploadedFileID.toString(); - } else { - return "https://public-albums.ente.io/download/?fileID=" + - uploadedFileID.toString(); - } - } - - String get pubPreviewUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/thumb/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + - "/public-collection/files/preview/" + - uploadedFileID.toString(); - } else { - return "https://public-albums.ente.io/preview/?fileID=" + - uploadedFileID.toString(); - } - } + String get downloadUrl => + FileUrl.getUrl(uploadedFileID!, FileUrlType.download); String? get caption { return pubMagicMetadata?.caption; @@ -290,18 +250,6 @@ class EnteFile { String? debugCaption; - String get thumbnailUrl { - if (localFileServer.isNotEmpty) { - return "$localFileServer/thumb/$uploadedFileID"; - } - final endpoint = Configuration.instance.getHttpEndpoint(); - if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) { - return endpoint + "/files/preview/" + uploadedFileID.toString(); - } else { - return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString(); - } - } - String get displayName { if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) { return pubMagicMetadata!.editedName!; diff --git a/mobile/lib/models/gallery_type.dart b/mobile/lib/models/gallery_type.dart index 82e5650b09..7afaf0433a 100644 --- a/mobile/lib/models/gallery_type.dart +++ b/mobile/lib/models/gallery_type.dart @@ -104,6 +104,7 @@ extension GalleyTypeExtension on GalleryType { bool showDeleteOption() { switch (this) { case GalleryType.ownedCollection: + case GalleryType.sharedCollection: case GalleryType.searchResults: case GalleryType.homepage: case GalleryType.favorite: @@ -119,7 +120,6 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.magic: return true; case GalleryType.trash: - case GalleryType.sharedCollection: case GalleryType.sharedPublicCollection: return false; } @@ -312,6 +312,7 @@ extension GalleryAppBarExtn on GalleryType { bool isSharable() { if (this == GalleryType.ownedCollection || this == GalleryType.quickLink || + this == GalleryType.favorite || this == GalleryType.hiddenOwnedCollection || this == GalleryType.sharedCollection) { return true; diff --git a/mobile/lib/models/memories/filler_memory.dart b/mobile/lib/models/memories/filler_memory.dart index 7fd597980c..e8e8293415 100644 --- a/mobile/lib/models/memories/filler_memory.dart +++ b/mobile/lib/models/memories/filler_memory.dart @@ -10,8 +10,8 @@ class FillerMemory extends SmartMemory { this.yearsAgo, int firstDateToShow, int lastDateToShow, { - int? firstCreationTime, - int? lastCreationTime, + super.firstCreationTime, + super.lastCreationTime, }) : super( memories, MemoryType.filler, diff --git a/mobile/lib/models/memories/memories_cache.dart b/mobile/lib/models/memories/memories_cache.dart index 7a5ba47507..949107901d 100644 --- a/mobile/lib/models/memories/memories_cache.dart +++ b/mobile/lib/models/memories/memories_cache.dart @@ -1,7 +1,7 @@ import "dart:convert"; +import "package:photos/models/base/id.dart"; import "package:photos/models/base_location.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/memories/clip_memory.dart"; import "package:photos/models/memories/people_memory.dart"; @@ -38,7 +38,6 @@ class MemoriesCache { factory MemoriesCache.fromJson( Map json, - Map filesMap, ) { return MemoriesCache( toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']), @@ -46,10 +45,7 @@ class MemoriesCache { clipShownLogs: ClipShownLog.decodeJsonToList(json['clipShownLogs']), tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']), baseLocations: json['baseLocations'] != null - ? BaseLocation.decodeJsonToList( - json['baseLocations'], - filesMap, - ) + ? BaseLocation.decodeJsonToList(json['baseLocations']) : [], ); } @@ -70,9 +66,8 @@ class MemoriesCache { static MemoriesCache decodeFromJsonString( String jsonString, - Map filesMap, ) { - return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap); + return MemoriesCache.fromJson(jsonDecode(jsonString)); } } @@ -83,6 +78,7 @@ class ToShowMemory { final int firstTimeToShow; final int lastTimeToShow; final int calculationTime; + final String id; final String? personID; final PeopleMemoryType? peopleMemoryType; @@ -99,7 +95,7 @@ class ToShowMemory { final relevantForNow = now >= firstTimeToShow && now < lastTimeToShow; final calculatedForNow = (now >= calculationTime) && (now < calculationTime + kMemoriesUpdateFrequency.inMicroseconds); - return relevantForNow && calculatedForNow; + return relevantForNow && (calculatedForNow || type == MemoryType.onThisDay); } ToShowMemory( @@ -108,6 +104,7 @@ class ToShowMemory { this.type, this.firstTimeToShow, this.lastTimeToShow, + this.id, this.calculationTime, { this.personID, this.peopleMemoryType, @@ -147,6 +144,7 @@ class ToShowMemory { memory.type, memory.firstDateToShow, memory.lastDateToShow, + memory.id, calcTime.microsecondsSinceEpoch, personID: personID, peopleMemoryType: peopleMemoryType, @@ -162,6 +160,7 @@ class ToShowMemory { memoryTypeFromString(json['type']), json['firstTimeToShow'], json['lastTimeToShow'], + json['id'] ?? newID(json['type'] as String), json['calculationTime'], personID: json['personID'], peopleMemoryType: json['peopleMemoryType'] != null @@ -186,6 +185,7 @@ class ToShowMemory { 'type': type.toString().split('.').last, 'firstTimeToShow': firstTimeToShow, 'lastTimeToShow': lastTimeToShow, + 'id': id, 'calculationTime': calculationTime, 'personID': personID, 'peopleMemoryType': peopleMemoryType?.toString().split('.').last, diff --git a/mobile/lib/models/memories/on_this_day_memory.dart b/mobile/lib/models/memories/on_this_day_memory.dart new file mode 100644 index 0000000000..de9e0e7ae3 --- /dev/null +++ b/mobile/lib/models/memories/on_this_day_memory.dart @@ -0,0 +1,24 @@ +import "package:photos/generated/l10n.dart"; +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +class OnThisDayMemory extends SmartMemory { + OnThisDayMemory( + List memories, + int firstDateToShow, + int lastDateToShow, { + super.firstCreationTime, + super.lastCreationTime, + }) : super( + memories, + MemoryType.onThisDay, + '', + firstDateToShow, + lastDateToShow, + ); + + @override + String createTitle(S s, String languageCode) { + return s.onThisDay; + } +} diff --git a/mobile/lib/models/memories/smart_memory.dart b/mobile/lib/models/memories/smart_memory.dart index b05ff64959..52ce8fbf64 100644 --- a/mobile/lib/models/memories/smart_memory.dart +++ b/mobile/lib/models/memories/smart_memory.dart @@ -1,4 +1,5 @@ import "package:photos/generated/l10n.dart"; +import "package:photos/models/base/id.dart"; import "package:photos/models/memories/memory.dart"; enum MemoryType { @@ -7,6 +8,7 @@ enum MemoryType { clip, time, filler, + onThisDay, } MemoryType memoryTypeFromString(String type) { @@ -21,6 +23,8 @@ MemoryType memoryTypeFromString(String type) { return MemoryType.filler; case "clip": return MemoryType.clip; + case "onThisDay": + return MemoryType.onThisDay; default: throw ArgumentError("Invalid memory type: $type"); } @@ -32,6 +36,7 @@ class SmartMemory { String title; int firstDateToShow; int lastDateToShow; + late final String id; int? firstCreationTime; int? lastCreationTime; @@ -42,9 +47,12 @@ class SmartMemory { this.title, this.firstDateToShow, this.lastDateToShow, { + String? id, this.firstCreationTime, this.lastCreationTime, - }); + }) { + this.id = id ?? newID(type.name); + } bool get notForShow => firstDateToShow == 0 && lastDateToShow == 0; diff --git a/mobile/lib/models/memories/time_memory.dart b/mobile/lib/models/memories/time_memory.dart index e723224aba..213ccba7c5 100644 --- a/mobile/lib/models/memories/time_memory.dart +++ b/mobile/lib/models/memories/time_memory.dart @@ -16,8 +16,8 @@ class TimeMemory extends SmartMemory { this.day, this.month, this.yearsAgo, - int? firstCreationTime, - int? lastCreationTime, + super.firstCreationTime, + super.lastCreationTime, }) : super( memories, MemoryType.time, diff --git a/mobile/lib/models/metadata/file_magic.dart b/mobile/lib/models/metadata/file_magic.dart index 02f6188a9d..53627aafbc 100644 --- a/mobile/lib/models/metadata/file_magic.dart +++ b/mobile/lib/models/metadata/file_magic.dart @@ -9,6 +9,7 @@ const captionKey = "caption"; const uploaderNameKey = "uploaderName"; const widthKey = 'w'; const heightKey = 'h'; +const streamVersionKey = 'sv'; const mediaTypeKey = 'mediaType'; const latKey = "lat"; const longKey = "long"; @@ -48,6 +49,10 @@ class PubMagicMetadata { double? lat; double? long; + // Indicates streaming version of the file. + // If this is set, then the file is a streaming version of the original file. + int? sv; + // ISO 8601 datetime without timezone. This contains the date and time of the photo in the original tz // where the photo was taken. String? dateTime; @@ -83,6 +88,7 @@ class PubMagicMetadata { this.mediaType, this.dateTime, this.offsetTime, + this.sv, }); factory PubMagicMetadata.fromEncodedJson(String encodedJson) => @@ -107,6 +113,7 @@ class PubMagicMetadata { mediaType: map[mediaTypeKey], dateTime: map[dateTimeKey], offsetTime: map[offsetTimeKey], + sv: safeParseInt(map[streamVersionKey], streamVersionKey), ); } diff --git a/mobile/lib/models/preview/playlist_data.dart b/mobile/lib/models/preview/playlist_data.dart index e5628dc131..7f40531017 100644 --- a/mobile/lib/models/preview/playlist_data.dart +++ b/mobile/lib/models/preview/playlist_data.dart @@ -1,5 +1,3 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; import "dart:io"; class PlaylistData { @@ -7,56 +5,13 @@ class PlaylistData { int? width; int? height; int? size; + int? durationInSeconds; PlaylistData({ required this.preview, this.width, this.height, this.size, + this.durationInSeconds, }); - - PlaylistData copyWith({ - File? preview, - int? width, - int? height, - int? size, - }) { - return PlaylistData( - preview: preview ?? this.preview, - width: width ?? this.width, - height: height ?? this.height, - size: size ?? this.size, - ); - } - - Map toMap() { - return { - 'preview': preview.readAsStringSync(), - 'width': width, - 'height': height, - 'size': size, - }; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() { - return 'PlaylistData(preview: $preview, width: $width, height: $height, size: $size)'; - } - - @override - bool operator ==(covariant PlaylistData other) { - if (identical(this, other)) return true; - - return other.preview == preview && - other.width == width && - other.height == height && - other.size == size; - } - - @override - int get hashCode { - return preview.hashCode ^ width.hashCode ^ height.hashCode ^ size.hashCode; - } } diff --git a/mobile/lib/models/search/search_constants.dart b/mobile/lib/models/search/search_constants.dart index 60c178192b..f05dfc50cb 100644 --- a/mobile/lib/models/search/search_constants.dart +++ b/mobile/lib/models/search/search_constants.dart @@ -3,3 +3,4 @@ const kPersonWidgetKey = 'person_widget_key'; const kClusterParamId = 'cluster_id'; const kFileID = 'file_id'; const kContactEmail = 'contact_email'; +const kContactCollections = 'contact_collections'; diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index db2c9be797..2a209cafc9 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -97,19 +97,14 @@ extension SectionTypeExtensions on SectionType { bool get isCTAVisible { switch (this) { case SectionType.face: - return false; case SectionType.magic: - return false; case SectionType.moment: - return false; - case SectionType.location: - return true; - case SectionType.contacts: - return true; - case SectionType.album: - return true; case SectionType.fileTypesAndExtension: return false; + case SectionType.location: + case SectionType.contacts: + case SectionType.album: + return true; } } @@ -117,24 +112,20 @@ extension SectionTypeExtensions on SectionType { bool get sortByName => this != SectionType.face && this != SectionType.magic && - this != SectionType.moment; + this != SectionType.moment && + this != SectionType.contacts; bool get isEmptyCTAVisible { switch (this) { case SectionType.face: - return false; case SectionType.magic: - return false; case SectionType.moment: - return false; - case SectionType.location: - return true; - case SectionType.contacts: - return true; - case SectionType.album: - return true; case SectionType.fileTypesAndExtension: return false; + case SectionType.location: + case SectionType.contacts: + case SectionType.album: + return true; } } @@ -236,21 +227,21 @@ extension SectionTypeExtensions on SectionType { } Future> getData( - BuildContext context, { + BuildContext? context, { int? limit, }) { switch (this) { case SectionType.face: return SearchService.instance.getAllFace(limit); case SectionType.magic: - return SearchService.instance.getMagicSectionResults(context); + return SearchService.instance.getMagicSectionResults(context!); case SectionType.moment: if (flagService.internalUser) { // TODO: lau: remove this whole smart memories and moment altogether - return SearchService.instance.smartMemories(context, limit); + return SearchService.instance.smartMemories(context!, limit); } - return SearchService.instance.getRandomMomentsSearchResults(context); + return SearchService.instance.getRandomMomentsSearchResults(context!); case SectionType.location: return SearchService.instance.getAllLocationTags(limit); @@ -263,7 +254,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.fileTypesAndExtension: return SearchService.instance - .getAllFileTypesAndExtensionsResults(context, limit); + .getAllFileTypesAndExtensionsResults(context!, limit); } } @@ -288,6 +279,8 @@ extension SectionTypeExtensions on SectionType { return [Bus.instance.on()]; case SectionType.magic: return [Bus.instance.on()]; + case SectionType.contacts: + return [Bus.instance.on()]; default: return []; } diff --git a/mobile/lib/models/selected_albums.dart b/mobile/lib/models/selected_albums.dart new file mode 100644 index 0000000000..0a09bafe05 --- /dev/null +++ b/mobile/lib/models/selected_albums.dart @@ -0,0 +1,46 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; +import 'package:photos/core/event_bus.dart'; +import "package:photos/events/clear_album_selections_event.dart"; +import 'package:photos/models/collection/collection.dart'; + +class SelectedAlbums extends ChangeNotifier { + final albums = {}; + + void toggleSelection(Collection albumToToggle) { + final Collection? alreadySelected = albums.firstWhereOrNull( + (element) => element.id == albumToToggle.id, + ); + if (alreadySelected != null) { + albums.remove(alreadySelected); + } else { + albums.add(albumToToggle); + } + notifyListeners(); + } + + void select(Set albumsToSelect) { + albums.addAll(albumsToSelect); + notifyListeners(); + } + + void unSelect( + Set albumsToUnselect, { + bool skipNotify = false, + }) { + albums.removeWhere((album) => albumsToUnselect.contains(album)); + if (!skipNotify) { + notifyListeners(); + } + } + + bool isAlbumSelected(Collection album) { + return albums.any((element) => element.id == album.id); + } + + void clearAll() { + Bus.instance.fire(ClearAlbumSelectionsEvent()); + albums.clear(); + notifyListeners(); + } +} diff --git a/mobile/lib/models/selected_people.dart b/mobile/lib/models/selected_people.dart new file mode 100644 index 0000000000..f9ba503d7d --- /dev/null +++ b/mobile/lib/models/selected_people.dart @@ -0,0 +1,42 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; + +class SelectedPeople extends ChangeNotifier { + final personIds = {}; + + void toggleSelection(String personID) { + final String? alreadySelected = personIds.firstWhereOrNull( + (element) => element == personID, + ); + if (alreadySelected != null) { + personIds.remove(alreadySelected); + } else { + personIds.add(personID); + } + notifyListeners(); + } + + void select(Set personToSelect) { + personIds.addAll(personToSelect); + notifyListeners(); + } + + void unSelect( + Set peopleToUnselect, { + bool skipNotify = false, + }) { + personIds.removeWhere((personID) => peopleToUnselect.contains(personID)); + if (!skipNotify) { + notifyListeners(); + } + } + + bool isPersonSelected(String personId) { + return personIds.any((element) => element == personId); + } + + void clearAll() { + personIds.clear(); + notifyListeners(); + } +} diff --git a/mobile/lib/module/download/file_url.dart b/mobile/lib/module/download/file_url.dart new file mode 100644 index 0000000000..e5faced127 --- /dev/null +++ b/mobile/lib/module/download/file_url.dart @@ -0,0 +1,43 @@ +import "package:photos/core/configuration.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/service_locator.dart"; + +enum FileUrlType { + download, + publicDownload, + thumbnail, + publicThumbnail, + directDownload, +} + +class FileUrl { + static String getUrl(int fileID, FileUrlType type) { + final endpoint = Configuration.instance.getHttpEndpoint(); + final disableWorker = + endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker; + + switch (type) { + case FileUrlType.directDownload: + return "$endpoint/files/download/$fileID"; + case FileUrlType.download: + return disableWorker + ? "$endpoint/files/download/$fileID" + : "https://files.ente.io/?fileID=$fileID"; + + case FileUrlType.publicDownload: + return disableWorker + ? "$endpoint/public-collection/files/download/$fileID" + : "https://public-albums.ente.io/download/?fileID=$fileID"; + + case FileUrlType.thumbnail: + return disableWorker + ? "$endpoint/files/preview/$fileID" + : "https://thumbnails.ente.io/?fileID=$fileID"; + + case FileUrlType.publicThumbnail: + return disableWorker + ? "$endpoint/public-collection/files/preview/$fileID" + : "https://public-albums.ente.io/preview/?fileID=$fileID"; + } + } +} diff --git a/mobile/lib/module/download/manager.dart b/mobile/lib/module/download/manager.dart new file mode 100644 index 0000000000..d46c278ba9 --- /dev/null +++ b/mobile/lib/module/download/manager.dart @@ -0,0 +1,417 @@ +import "dart:async"; +import "dart:io"; + +import "package:dio/dio.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/configuration.dart"; + +import "package:photos/module/download/file_url.dart"; +import "package:photos/module/download/task.dart"; + +class DownloadManager { + final _logger = Logger('DownloadManager'); + static const int downloadChunkSize = 40 * 1024 * 1024; + + final Dio _dio; + + // In-memory storage for download tasks + final Map _tasks = {}; + + // Active downloads with their completers and streams + final Map> _completers = {}; + final Map> _streams = {}; + final Map _cancelTokens = {}; + final Map _downloadStartTimes = {}; + + DownloadManager(this._dio); + + /// Subscribe to download progress updates for a specific file ID + Stream watchDownload(int fileId) { + _streams[fileId] ??= StreamController.broadcast(); + return _streams[fileId]!.stream; + } + + bool enableResumableDownload(int? size) { + if (size == null) return false; + //todo: Use FileUrlType.direct instead of FileUrlType.directDownload + return size > downloadChunkSize; + } + + /// Start download and return a Future that completes when download finishes + /// If download was paused, calling this again will resume it + Future download( + int fileId, + String filename, + int totalBytes, + ) async { + _downloadStartTimes[fileId] = DateTime.now().microsecondsSinceEpoch; + // If already downloading, return existing future + if (_completers.containsKey(fileId)) { + return _completers[fileId]!.future; + } + + final completer = Completer(); + _completers[fileId] = completer; + + // Get or create task + final existingTask = _tasks[fileId]; + final task = existingTask ?? + DownloadTask( + id: fileId, + filename: filename, + totalBytes: totalBytes, + ); + + // Store task in memory + _tasks[fileId] = task; + + // Don't restart if already completed + if (task.isCompleted) { + // ensure that the file exists + final filePath = task.filePath; + if (filePath == null || !(await File(filePath).exists())) { + // If the file doesn't exist, mark the task as error + _logger.warning( + 'File not found for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + final updatedTask = task.copyWith( + status: DownloadStatus.error, + error: 'File not found', + filePath: null, + ); + _updateTask(updatedTask); + final result = DownloadResult(updatedTask, false); + completer.complete(result); + return result; + } else { + _logger.info( + 'Download already completed for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + final result = DownloadResult(task, true); + completer.complete(result); + return result; + } + } + unawaited(_startDownload(task, completer)); + return completer.future; + } + + /// Pause download + Future pause(int fileId) async { + // ignore cancel if download started less than 1 second ago, + // this is to avoid cancellination due to different type of video players, where dispose is called + // little later after other video player operations + final startTime = _downloadStartTimes[fileId]; + if (startTime == null || + DateTime.now().microsecondsSinceEpoch - startTime < 1e6) { + _logger.info('Download paused too soon, ignoring pause request'); + return; + } + final token = _cancelTokens[fileId]; + if (token != null && !token.isCancelled) { + token.cancel('paused'); + } + + final task = _tasks[fileId]; + if (task != null && task.isActive) { + _updateTask(task.copyWith(status: DownloadStatus.paused)); + } + + // Clean up streams if no listeners + final stream = _streams[fileId]; + if (stream != null && !stream.hasListener) { + await stream.close(); + _streams.remove(fileId); + } + } + + /// Cancel and delete download + Future cancel(int fileId) async { + final token = _cancelTokens[fileId]; + if (token != null && !token.isCancelled) { + token.cancel('cancelled'); + } + + final task = _tasks[fileId]; + if (task != null) { + await _deleteFiles(task); + _updateTask(task.copyWith(status: DownloadStatus.cancelled)); + _tasks.remove(fileId); + } + _cleanup(fileId); + } + + /// Get current download status + Future getDownload(int fileId) async => _tasks[fileId]; + + /// Get all downloads + Future> getAllDownloads() async => _tasks.values.toList(); + + Future _startDownload( + DownloadTask task, + Completer completer, + ) async { + try { + task = task.copyWith(status: DownloadStatus.downloading); + _updateTask(task); + + final cancelToken = CancelToken(); + _cancelTokens[task.id] = cancelToken; + + final directory = Configuration.instance.getTempDirectory(); + final basePath = '$directory${task.id}.encrypted'; + + // check if base file already exists and is of correct size + final baseFile = File(basePath); + if (await baseFile.exists()) { + final existingSize = await baseFile.length(); + if (existingSize == task.totalBytes) { + _logger.info( + 'Download already exists for ${task.filename} ($existingSize/${task.totalBytes} bytes)', + ); + task = task.copyWith( + status: DownloadStatus.completed, + filePath: basePath, + bytesDownloaded: existingSize, + ); + _updateTask(task); + completer.complete(DownloadResult(task, true)); + return; + } else { + _logger.warning( + 'Existing file size mismatch for ${task.filename}: ' + 'expected ${task.totalBytes}, but got $existingSize', + ); + await baseFile.delete(); // Remove corrupted file + } + } + + // Check existing chunks and calculate progress + final totalChunks = (task.totalBytes / downloadChunkSize).ceil(); + final existingChunks = + await _validateExistingChunks(basePath, task.totalBytes, totalChunks); + + task = task.copyWith( + bytesDownloaded: _calculateDownloadedBytes( + existingChunks, + task.totalBytes, + totalChunks, + ), + ); + _updateTask(task); + + _logger.info( + 'Resuming download for ${task.filename} (${task.bytesDownloaded}/${task.totalBytes} bytes)', + ); + for (int i = 0; i < totalChunks; i++) { + if (existingChunks[i]) { + continue; + } + if (cancelToken.isCancelled) { + _logger.info('Download cancelled for ${task.filename}'); + break; + } + await _downloadChunk(task, basePath, i, totalChunks, cancelToken); + existingChunks[i] = true; + } + + if (!cancelToken.isCancelled) { + final finalPath = await _combineChunks(basePath, totalChunks); + task = task.copyWith( + status: DownloadStatus.completed, + filePath: finalPath, + bytesDownloaded: task.totalBytes, + ); + _updateTask(task); + completer.complete(DownloadResult(task, true)); + } + } catch (e) { + if (e is DioException && e.type == DioExceptionType.cancel) { + _logger.info('Download cancelled for ${task.filename}'); + // Complete future with current task state (paused or cancelled) + final currentTask = _tasks[task.id]; + if (currentTask != null && !completer.isCompleted) { + completer.complete(DownloadResult(currentTask, false)); + } + return; + } + _logger.warning('Error downloading ${task.filename}', e); + task = task.copyWith(status: DownloadStatus.error, error: e.toString()); + _updateTask(task); + if (!completer.isCompleted) { + completer.complete(DownloadResult(task, false)); + } + } finally { + _cleanup(task.id); + } + } + + String _getChunkPath(String basePath, int part) { + return '$basePath.${part}_part'; + } + + Future> _validateExistingChunks( + String basePath, + int totalBytes, + int totalChunks, + ) async { + final existingChunks = List.filled(totalChunks, false); + + for (int i = 0; i < totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i + 1)); + if (!await chunkFile.exists()) continue; + + final expectedSize = i == totalChunks - 1 + ? totalBytes - (i * downloadChunkSize) + : downloadChunkSize; + + final actualSize = await chunkFile.length(); + if (actualSize == expectedSize) { + _logger.info('existing chunk ${i + 1} is valid'); + existingChunks[i] = true; + } else { + _logger.warning( + 'Chunk ${i + 1} is corrupted: expected $expectedSize bytes, ' + 'but got $actualSize bytes', + ); + existingChunks[i] = false; + // await chunkFile.delete(); // Remove corrupted chunk + } + } + + return existingChunks; + } + + int _calculateDownloadedBytes( + List existingChunks, + int totalBytes, + int totalChunks, + ) { + int bytes = 0; + for (int i = 0; i < existingChunks.length; i++) { + if (existingChunks[i]) { + bytes += i == totalChunks - 1 + ? totalBytes - (i * downloadChunkSize) + : downloadChunkSize; + } + } + return bytes; + } + + Future _downloadChunk( + DownloadTask task, + String basePath, + int chunkIndex, + int totalChunks, + CancelToken cancelToken, + ) async { + final chunkPath = _getChunkPath(basePath, chunkIndex + 1); + final startByte = chunkIndex * downloadChunkSize; + final endByte = chunkIndex == totalChunks - 1 + ? task.totalBytes - 1 + : (startByte + downloadChunkSize) - 1; + _logger.info('Downloading chunk ${chunkIndex + 1}/$totalChunks'); + await _dio.download( + FileUrl.getUrl(task.id, FileUrlType.directDownload), + chunkPath, + queryParameters: { + "token": Configuration.instance.getToken(), + }, + options: Options( + headers: { + "Range": "bytes=$startByte-$endByte", + }, + ), + cancelToken: cancelToken, + onReceiveProgress: (received, total) async { + final updatedTask = task.copyWith( + bytesDownloaded: (chunkIndex) * downloadChunkSize + received, + ); + _notifyProgress(updatedTask); + }, + ); + // Update progress after chunk completion + final chunkFileSize = await File(chunkPath).length(); + task = task.copyWith( + bytesDownloaded: (chunkIndex) * downloadChunkSize + chunkFileSize, + ); + _updateTask(task); + } + + Future _combineChunks(String basePath, int totalChunks) async { + final finalFile = File(basePath); + final sink = finalFile.openWrite(); + try { + for (int i = 1; i <= totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i)); + await sink.addStream(chunkFile.openRead()); + await chunkFile.delete(); + } + } finally { + await sink.close(); + } + return finalFile.path; + } + + Future _deleteFiles(DownloadTask task) async { + try { + final directory = Configuration.instance.getTempDirectory(); + final basePath = '$directory${task.id}.encrypted'; + final finalFile = File(basePath); + if (await finalFile.exists()) await finalFile.delete(); + + // Delete chunk files + final totalChunks = (task.totalBytes / downloadChunkSize).ceil(); + for (int i = 1; i <= totalChunks; i++) { + final chunkFile = File(_getChunkPath(basePath, i)); + if (await chunkFile.exists()) await chunkFile.delete(); + } + } catch (e) { + _logger.warning('Error deleting files: $e'); + } + } + + void _updateTask(DownloadTask task) { + _tasks[task.id] = task; + _notifyProgress(task); + } + + void _notifyProgress(DownloadTask task) { + final stream = _streams[task.id]; + if (stream != null && !stream.isClosed) { + stream.add(task); + } + } + + void _cleanup(int fileId) { + _completers.remove(fileId); + _cancelTokens.remove(fileId); + + final stream = _streams[fileId]; + if (stream != null && !stream.hasListener) { + stream.close(); + _streams.remove(fileId); + } + } + + Future dispose() async { + for (final completer in _completers.values) { + if (!completer.isCompleted) { + completer.completeError('Disposed'); + } + } + _completers.clear(); + + for (final token in _cancelTokens.values) { + token.cancel('Disposed'); + } + _cancelTokens.clear(); + + for (final stream in _streams.values) { + await stream.close(); + } + _streams.clear(); + + _tasks.clear(); + } +} diff --git a/mobile/lib/module/download/task.dart b/mobile/lib/module/download/task.dart new file mode 100644 index 0000000000..1bd96ca295 --- /dev/null +++ b/mobile/lib/module/download/task.dart @@ -0,0 +1,80 @@ +enum DownloadStatus { + pending, + downloading, + paused, + completed, + error, + cancelled +} + +class DownloadTask { + final int id; + final String filename; + final int totalBytes; + int bytesDownloaded; + DownloadStatus status; + String? error; + String? filePath; + + DownloadTask({ + required this.id, + required this.filename, + required this.totalBytes, + this.bytesDownloaded = 0, + this.status = DownloadStatus.pending, + this.error, + this.filePath, + }); + + double get progress => totalBytes > 0 ? bytesDownloaded / totalBytes : 0.0; + bool get isCompleted => status == DownloadStatus.completed; + bool get isActive => status == DownloadStatus.downloading; + bool get isFinished => [ + DownloadStatus.completed, + DownloadStatus.error, + DownloadStatus.cancelled, + ].contains(status); + + Map toMap() => { + 'id': id, + 'filename': filename, + 'totalBytes': totalBytes, + 'bytesDownloaded': bytesDownloaded, + 'status': status.name, + 'error': error, + 'filePath': filePath, + }; + + static DownloadTask fromMap(Map map) => DownloadTask( + id: map['id'], + filename: map['filename'], + totalBytes: map['totalBytes'], + bytesDownloaded: map['bytesDownloaded'] ?? 0, + status: DownloadStatus.values.byName(map['status']), + error: map['error'], + filePath: map['filePath'], + ); + + DownloadTask copyWith({ + int? bytesDownloaded, + DownloadStatus? status, + String? error, + String? filePath, + }) => + DownloadTask( + id: id, + filename: filename, + totalBytes: totalBytes, + bytesDownloaded: bytesDownloaded ?? this.bytesDownloaded, + status: status ?? this.status, + error: error ?? this.error, + filePath: filePath ?? this.filePath, + ); +} + +class DownloadResult { + final DownloadTask task; + final bool success; + + DownloadResult(this.task, this.success); +} diff --git a/mobile/lib/module/upload/service/multipart.dart b/mobile/lib/module/upload/service/multipart.dart index f4ecf3b3c4..d547a45938 100644 --- a/mobile/lib/module/upload/service/multipart.dart +++ b/mobile/lib/module/upload/service/multipart.dart @@ -54,7 +54,7 @@ class MultiPartUploader { return multipartPartSize; } - Future calculatePartCount(int fileSize) async { + int calculatePartCount(int fileSize) { // If the feature flag is disabled, return 1 if (!_featureFlagService.enableMobMultiPart) return 1; if (!localSettings.userEnabledMultiplePart) return 1; diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 57728a7a26..bc96877eb2 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -4,11 +4,13 @@ import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:package_info_plus/package_info_plus.dart"; import "package:photos/gateways/entity_gw.dart"; +import "package:photos/module/download/manager.dart"; import "package:photos/services/account/billing_service.dart"; import "package:photos/services/entity_service.dart"; +import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/location_service.dart"; +import "package:photos/services/machine_learning/compute_controller.dart"; import "package:photos/services/machine_learning/face_ml/face_recognition_service.dart"; -import "package:photos/services/machine_learning/machine_learning_controller.dart"; import "package:photos/services/magic_cache_service.dart"; import "package:photos/services/memories_cache_service.dart"; import "package:photos/services/permission/service.dart"; @@ -22,6 +24,7 @@ import "package:shared_preferences/shared_preferences.dart"; class ServiceLocator { late final SharedPreferences prefs; late final Dio enteDio; + late final Dio nonEnteDio; late final PackageInfo packageInfo; // instance @@ -29,9 +32,15 @@ class ServiceLocator { static final ServiceLocator instance = ServiceLocator._privateConstructor(); - init(SharedPreferences prefs, Dio enteDio, PackageInfo packageInfo) { + init( + SharedPreferences prefs, + Dio enteDio, + Dio nonEnteDio, + PackageInfo packageInfo, + ) { this.prefs = prefs; this.enteDio = enteDio; + this.nonEnteDio = nonEnteDio; this.packageInfo = packageInfo; } } @@ -133,10 +142,10 @@ BillingService get billingService { return _billingService!; } -MachineLearningController? _machineLearningController; -MachineLearningController get machineLearningController { - _machineLearningController ??= MachineLearningController(); - return _machineLearningController!; +ComputeController? _computeController; +ComputeController get computeController { + _computeController ??= ComputeController(); + return _computeController!; } FaceRecognitionService? _faceRecognitionService; @@ -150,3 +159,20 @@ PermissionService get permissionService { _permissionService ??= PermissionService(ServiceLocator.instance.prefs); return _permissionService!; } + +FileDataService? _fileDataService; +FileDataService get fileDataService { + _fileDataService ??= FileDataService( + ServiceLocator.instance.prefs, + ServiceLocator.instance.enteDio, + ); + return _fileDataService!; +} + +DownloadManager? _downloadManager; +DownloadManager get downloadManager { + _downloadManager ??= DownloadManager( + ServiceLocator.instance.nonEnteDio, + ); + return _downloadManager!; +} diff --git a/mobile/lib/services/account/user_service.dart b/mobile/lib/services/account/user_service.dart index ab4801d94b..00f6842d97 100644 --- a/mobile/lib/services/account/user_service.dart +++ b/mobile/lib/services/account/user_service.dart @@ -1445,4 +1445,19 @@ class UserService { return emailIDs; } + + Set getEmailIDsOfFamilyMember() { + final emailIDs = {}; + + final cachedUserDetails = getCachedUserDetails(); + if (cachedUserDetails?.familyData?.members?.isNotEmpty ?? false) { + for (final member in cachedUserDetails!.familyData!.members!) { + if (member.email.isNotEmpty) { + emailIDs.add(member.email); + } + } + } + + return emailIDs; + } } diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart new file mode 100644 index 0000000000..4bf8864940 --- /dev/null +++ b/mobile/lib/services/album_home_widget_service.dart @@ -0,0 +1,484 @@ +import 'dart:convert'; +import "dart:math"; + +import "package:collection/collection.dart"; +import 'package:crypto/crypto.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/collection/collection.dart'; +import 'package:photos/models/collection/collection_items.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/collections_service.dart'; +import 'package:photos/services/favorites_service.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import "package:photos/ui/viewer/file/detail_page.dart"; +import 'package:photos/ui/viewer/gallery/collection_page.dart'; +import 'package:photos/utils/navigation_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; + +class AlbumHomeWidgetService { + // Constants + static const String SELECTED_ALBUMS_KEY = "selectedAlbumsHW"; + static const String ALBUMS_LAST_HASH_KEY = "albumsLastHash"; + static const String ANDROID_CLASS_NAME = "EnteAlbumsWidgetProvider"; + static const String IOS_CLASS_NAME = "EnteAlbumWidget"; + static const String ALBUMS_CHANGED_KEY = "albumsChanged.widget"; + static const String ALBUMS_STATUS_KEY = "albumsStatusKey.widget"; + static const String TOTAL_ALBUMS_KEY = "totalAlbums"; + static const int MAX_ALBUMS_LIMIT = 50; + + // Singleton pattern + static final AlbumHomeWidgetService instance = + AlbumHomeWidgetService._privateConstructor(); + AlbumHomeWidgetService._privateConstructor(); + + // Properties + final Logger _logger = Logger((AlbumHomeWidgetService).toString()); + late final SharedPreferences _prefs; + final _albumsForceRefreshLock = Lock(); + bool _hasSyncedAlbums = false; + + // Initialization + void init(SharedPreferences prefs) { + _prefs = prefs; + } + + // Public methods + Future initHomeWidget(bool? forceFetchNewAlbums) async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _albumsForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + final isWidgetEmpty = await _isWidgetEmpty(); + forceFetchNewAlbums ??= await _shouldForceFetchAlbums(isWidgetEmpty); + + _logger.warning( + "Initializing albums widget: forceFetch: $forceFetchNewAlbums, isEmpty: $isWidgetEmpty", + ); + + if (forceFetchNewAlbums!) { + await _forceAlbumsUpdate(); + } else if (!isWidgetEmpty) { + await _syncExistingAlbums(); + } + }); + } + + List? getSelectedAlbumIds() { + final selectedAlbums = _prefs.getStringList(SELECTED_ALBUMS_KEY); + return selectedAlbums?.map((id) => int.tryParse(id) ?? 0).toList(); + } + + Future setSelectedAlbums(List selectedAlbums) async { + await _prefs.setStringList(SELECTED_ALBUMS_KEY, selectedAlbums); + } + + String? getAlbumsLastHash() { + return _prefs.getString(ALBUMS_LAST_HASH_KEY); + } + + Future setAlbumsLastHash(String hash) async { + await _prefs.setString(ALBUMS_LAST_HASH_KEY, hash); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future clearWidget() async { + if (await _isWidgetEmpty()) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing AlbumsHomeWidget"); + await _setTotalAlbums(null); + await updateAlbumsStatus(WidgetStatus.syncedEmpty); + _hasSyncedAlbums = false; + await _refreshWidget(message: "AlbumsHomeWidget cleared & updated"); + } + + bool getAlbumsChanged() { + return _prefs.getBool(ALBUMS_CHANGED_KEY) ?? false; + } + + Future updateAlbumsChanged(bool value) async { + _logger.info("Updating albums changed flag to $value"); + await _prefs.setBool(ALBUMS_CHANGED_KEY, value); + } + + WidgetStatus getAlbumsStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(ALBUMS_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updateAlbumsStatus(WidgetStatus value) async { + await _prefs.setInt(ALBUMS_STATUS_KEY, value.index); + } + + Future checkPendingAlbumsSync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isWidgetEmpty = await _isWidgetEmpty(); + final shouldForceFetch = await _shouldForceFetchAlbums(isWidgetEmpty); + + if (_hasSyncedAlbums && !shouldForceFetch) { + _logger.info("Albums already synced, no action needed"); + return; + } + + await initHomeWidget(shouldForceFetch); + } + + Future albumsChanged() async { + final lastHash = getAlbumsLastHash(); + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final currentHash = _calculateHash(selectedAlbumIds); + + if (selectedAlbumIds.isEmpty || currentHash == lastHash) { + _logger.info("No changes detected in albums"); + return; + } + + _logger.info("Albums changed, updating widget"); + await updateAlbumsChanged(true); + await initHomeWidget(true); + } + + List getAlbumsByIds(List albumIds) { + final albums = []; + + for (final albumId in albumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + albums.add(collection); + } + } + + return albums; + } + + String _calculateHash(List albumIds) { + String updationTimestamps = ""; + + for (final albumId in albumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + updationTimestamps += "${collection.updationTime.toString()}_"; + } + } + + final hash = md5 + .convert(utf8.encode(updationTimestamps)) + .toString() + .substring(0, 10); + return hash; + } + + Future onLaunchFromWidget( + int fileId, + int collectionId, + BuildContext context, + ) async { + _hasSyncedAlbums = true; + await _syncExistingAlbums(); + + final collection = + CollectionsService.instance.getCollectionByID(collectionId); + if (collection == null) { + _logger.warning( + "Cannot launch widget: collection with ID $collectionId not found", + ); + return; + } + + // First navigate to the collection page + final thumbnail = await CollectionsService.instance.getCover(collection); + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(collection, thumbnail), + ), + ).ignore(); + final getAllFilesCollection = + await FilesDB.instance.getAllFilesCollection(collection.id); + + // Then open the specific file + final file = await FilesDB.instance.getFile(fileId); + if (file == null) { + _logger.warning("Cannot launch widget: file with ID $fileId not found"); + return; + } + + await routeToPage( + context, + DetailPage( + DetailPageConfiguration( + getAllFilesCollection, + getAllFilesCollection.indexOf(file), + "albumwidget", + ), + ), + forceCustomPageRoute: true, + ); + } + + // Private methods + Future _hasAnyBlockers() async { + // Check if first import is completed + final hasCompletedFirstImport = + LocalSyncService.instance.hasCompletedFirstImport(); + if (!hasCompletedFirstImport) { + _logger.warning("First import not completed"); + return true; + } + + // Check if selected albums exist + final selectedAlbumIds = getSelectedAlbumIds(); + final albums = getAlbumsByIds(selectedAlbumIds ?? []); + + if ((selectedAlbumIds?.isNotEmpty ?? false) && albums.isEmpty) { + _logger.warning("Selected albums not found"); + return true; + } + + return false; + } + + Future _forceAlbumsUpdate() async { + await _loadAndRenderAlbums(); + await updateAlbumsChanged(false); + } + + Future _syncExistingAlbums() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); + return; + } + + await _refreshWidget(message: "Refreshing from existing album set"); + } + + Future _isWidgetEmpty() async { + final totalAlbums = await _getTotalAlbums(); + return totalAlbums == 0 || totalAlbums == null; + } + + Future _shouldForceFetchAlbums(bool isWidgetEmpty) async { + // Check if albums changed flag is set + final albumsChanged = _prefs.getBool(ALBUMS_CHANGED_KEY); + if (albumsChanged == true) { + return true; + } + + // Check if we have any albums selected + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + if (selectedAlbumIds.isEmpty) { + _logger.warning("No albums selected"); + return false; + } + + // Check if hash has changed + final currentHash = _calculateHash(selectedAlbumIds); + final lastHash = getAlbumsLastHash(); + + if (currentHash == lastHash) { + final saveStatus = getAlbumsStatus(); + + switch (saveStatus) { + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; + default: + } + } + + return true; + } + + Future> _getEffectiveSelectedAlbumIds() async { + final selectedAlbumIds = getSelectedAlbumIds(); + + // If no albums selected, use favorites as default + if (selectedAlbumIds == null || selectedAlbumIds.isEmpty) { + final favoriteId = + await FavoritesService.instance.getFavoriteCollectionID(); + if (favoriteId != null) { + return [favoriteId]; + } + } + + return selectedAlbumIds ?? []; + } + + Future _getTotalAlbums() async { + return HomeWidgetService.instance.getData(TOTAL_ALBUMS_KEY); + } + + Future _setTotalAlbums(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_ALBUMS_KEY, total); + } + + Future _refreshWidget({String? message}) async { + await HomeWidgetService.instance.updateWidget( + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, + ); + + if (flagService.internalUser) { + await Fluttertoast.showToast( + msg: "[i][al] ${message ?? "AlbumsHomeWidget updated"}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); + } + + Future)>> _getAlbumsWithFiles() async { + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final albumsWithFiles = )>{}; + + for (final albumId in selectedAlbumIds) { + final collection = CollectionsService.instance.getCollectionByID(albumId); + if (collection != null) { + final files = + await FilesDB.instance.getAllFilesCollection(collection.id); + if (files.isNotEmpty) { + albumsWithFiles[collection.id] = + (collection.decryptedName ?? "Album", files); + } + } + } + + if (albumsWithFiles.isEmpty) { + _logger.warning("No albums with files found"); + } + + return albumsWithFiles; + } + + Future _loadAndRenderAlbums() async { + final albumsWithFiles = await _getAlbumsWithFiles(); + + if (albumsWithFiles.isEmpty) { + _logger.warning("No files found for any albums, clearing widget"); + await clearWidget(); + return; + } + + final currentTotal = await _getTotalAlbums(); + _logger.info("Current total albums in widget: $currentTotal"); + + final bool isWidgetPresent = await countHomeWidgets() > 0; + + final limit = isWidgetPresent ? MAX_ALBUMS_LIMIT : 5; + final maxAttempts = limit * 10; + + int renderedCount = 0; + int attemptsCount = 0; + + await updateAlbumsStatus(WidgetStatus.notSynced); + + final albumsWithFilesLength = albumsWithFiles.length; + final albumsWithFilesEntries = albumsWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + albumsWithFilesEntries[random.nextInt(albumsWithFilesLength)]; + + if (randomEntry.value.$2.isEmpty) continue; + + final randomAlbumFile = randomEntry.value.$2.elementAt( + random.nextInt(randomEntry.value.$2.length), + ); + final albumId = randomEntry.key; + final albumName = randomEntry.value.$1; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomAlbumFile, + "albums_widget_$renderedCount", + albumName, + albumId.toString(), + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _setTotalAlbums(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First album fetched, updating widget", + ); + await updateAlbumsStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; + } + + attemptsCount++; + } + + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + // Update the hash to track changes + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final hash = _calculateHash(selectedAlbumIds); + await setAlbumsLastHash(hash); + + if (renderedCount == 0) { + return; + } + + if (isWidgetPresent) { + await updateAlbumsStatus(WidgetStatus.syncedAll); + } + + await _refreshWidget( + message: "Switched to next albums set, total: $renderedCount", + ); + } +} diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 3f00218e32..ce0addb91b 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -388,7 +388,20 @@ class CollectionsService { .toList(); } - SharedCollections getSharedCollections() { + List getAllOwnedCollectionIDs() { + final int userID = _config.getUserID()!; + return _collectionIDToCollections.values + .where( + (c) => !c.isDeleted && c.isOwner(userID), + ) + .map((e) => e.id) + .toList(); + } + + Future getSharedCollections() async { + final AlbumSortKey sortKey = localSettings.albumSortKey(); + final AlbumSortDirection sortDirection = localSettings.albumSortDirection(); + final List outgoing = []; final List incoming = []; final List quickLinks = []; @@ -405,17 +418,65 @@ class CollectionsService { incoming.add(c); } } - incoming.sort((first, second) { - return second.updationTime.compareTo(first.updationTime); - }); - outgoing.sort((first, second) { - return second.updationTime.compareTo(first.updationTime); - }); + + late Map collectionIDToNewestPhotoTime; + if (sortKey == AlbumSortKey.newestPhoto) { + collectionIDToNewestPhotoTime = + await CollectionsService.instance.getCollectionIDToNewestFileTime(); + } + + incoming.sort( + (first, second) { + int comparison; + if (sortKey == AlbumSortKey.albumName) { + comparison = compareAsciiLowerCaseNatural( + first.displayName, + second.displayName, + ); + } else if (sortKey == AlbumSortKey.newestPhoto) { + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( + collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, + ); + } else { + comparison = second.updationTime.compareTo(first.updationTime); + } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; + }, + ); + + outgoing.sort( + (first, second) { + int comparison; + if (sortKey == AlbumSortKey.albumName) { + comparison = compareAsciiLowerCaseNatural( + first.displayName, + second.displayName, + ); + } else if (sortKey == AlbumSortKey.newestPhoto) { + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( + collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, + ); + } else { + comparison = second.updationTime.compareTo(first.updationTime); + } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; + }, + ); + return SharedCollections(outgoing, incoming, quickLinks); } Future> getCollectionForOnEnteSection() async { final AlbumSortKey sortKey = localSettings.albumSortKey(); + final AlbumSortDirection sortDirection = localSettings.albumSortDirection(); final List collections = CollectionsService.instance.getCollectionsForUI(); final bool hasFavorites = FavoritesService.instance.hasFavorites(); @@ -426,19 +487,24 @@ class CollectionsService { } collections.sort( (first, second) { + int comparison; if (sortKey == AlbumSortKey.albumName) { - return compareAsciiLowerCaseNatural( + comparison = compareAsciiLowerCaseNatural( first.displayName, second.displayName, ); } else if (sortKey == AlbumSortKey.newestPhoto) { - return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) - .compareTo( + comparison = + (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue) + .compareTo( collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue, ); } else { - return second.updationTime.compareTo(first.updationTime); + comparison = second.updationTime.compareTo(first.updationTime); } + return sortDirection == AlbumSortDirection.ascending + ? comparison + : -comparison; }, ); final List favorites = []; @@ -1194,7 +1260,16 @@ class CollectionsService { return null; } - /// Is a public link opened in the app + Map publicCollectionHeaders(int collectionID) { + final String? albumToken = _cachedPublicAlbumToken[collectionID]; + final String? albumJwtToken = _cachedPublicAlbumJWT[collectionID]; + return { + if (albumToken != null) "X-Auth-Access-Token": albumToken, + if (albumJwtToken != null) "X-Auth-Access-Token-JWT": albumJwtToken, + }; + } + + /// Is a public link opened in the app via deeplink bool isSharedPublicLink(int collectionID) { return _cachedPublicCollectionID.contains(collectionID); } diff --git a/mobile/lib/services/favorites_service.dart b/mobile/lib/services/favorites_service.dart index 68cdc59944..9b8d9d6b59 100644 --- a/mobile/lib/services/favorites_service.dart +++ b/mobile/lib/services/favorites_service.dart @@ -60,7 +60,7 @@ class FavoritesService { } Future _warmUpCache() async { - final favCollection = await _getFavoritesCollection(); + final favCollection = await getFavoritesCollection(); if (favCollection != null) { Set uploadedIDs; Map fileHashes; @@ -103,7 +103,7 @@ class FavoritesService { } Future isFavorite(EnteFile file) async { - final collection = await _getFavoritesCollection(); + final collection = await getFavoritesCollection(); if (collection == null || file.uploadedFileID == null) { return false; } @@ -176,7 +176,7 @@ class FavoritesService { if (favFlag) { await _collectionsService.addOrCopyToCollection(collectionID, files); } else { - final Collection? favCollection = await _getFavoritesCollection(); + final Collection? favCollection = await getFavoritesCollection(); await _collectionActions.moveFilesFromCurrentCollection( context, favCollection!, @@ -194,7 +194,7 @@ class FavoritesService { if (inUploadID == null) { // Do nothing, ignore } else { - final Collection? favCollection = await _getFavoritesCollection(); + final Collection? favCollection = await getFavoritesCollection(); // The file might be part of another collection. For unfav, we need to // move file from the fav collection to the . if (file.ownerID != _config.getUserID() && @@ -225,7 +225,7 @@ class FavoritesService { _updateFavoriteFilesCache([file], favFlag: false); } - Future _getFavoritesCollection() async { + Future getFavoritesCollection() async { if (_cachedFavoritesCollectionID == null) { final collections = _collectionsService.getActiveCollections(); for (final collection in collections) { @@ -241,7 +241,7 @@ class FavoritesService { } Future getFavoriteCollectionID() async { - final collection = await _getFavoritesCollection(); + final collection = await getFavoritesCollection(); return collection?.id; } diff --git a/mobile/lib/services/filedata/filedata_service.dart b/mobile/lib/services/filedata/filedata_service.dart index dd01fa36e4..5407011313 100644 --- a/mobile/lib/services/filedata/filedata_service.dart +++ b/mobile/lib/services/filedata/filedata_service.dart @@ -1,9 +1,9 @@ import "dart:async"; import "package:computer/computer.dart"; +import "package:dio/dio.dart"; import "package:flutter/foundation.dart" show Uint8List; import "package:logging/logging.dart"; -import "package:photos/core/network/network.dart"; import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/db/ml/filedata.dart"; @@ -16,26 +16,23 @@ import "package:photos/utils/gzip.dart"; import "package:shared_preferences/shared_preferences.dart"; class FileDataService { - FileDataService._privateConstructor(); - static final Computer _computer = Computer.shared(); - static final FileDataService instance = FileDataService._privateConstructor(); final _logger = Logger("FileDataService"); - final _dio = NetworkClient.instance.enteDio; - late final SharedPreferences _prefs; - Map? previewIds; + final Dio _dio; + final SharedPreferences _prefs; + late Map previewIds; - void init(SharedPreferences prefs) { - _prefs = prefs; + FileDataService(this._prefs, this._dio) { + _logger.info("FileDataService constructor called"); + previewIds = {}; } /// Used to not sync preview ids everytime a chunking and preview /// upload is successful, instead update the local copy of those /// preview ids void appendPreview(int id, String objectId, int objectSize) { - if (previewIds?.containsKey(id) ?? false) return; - previewIds ??= {}; - previewIds?[id] = PreviewInfo( + if (previewIds.containsKey(id)) return; + previewIds[id] = PreviewInfo( objectId: objectId, objectSize: objectSize, ); @@ -152,6 +149,9 @@ class FileDataService { previewIds = await MLDataDB.instance.getFileIDsVidPreview(); } while (hasMoreData); } catch (e) { + if (previewIds.isEmpty) { + previewIds = await MLDataDB.instance.getFileIDsVidPreview(); + } _logger.severe("Failed to syncDiff", e); rethrow; } diff --git a/mobile/lib/services/filter/db_filters.dart b/mobile/lib/services/filter/db_filters.dart index 5329b4825c..a4ec0bce9d 100644 --- a/mobile/lib/services/filter/db_filters.dart +++ b/mobile/lib/services/filter/db_filters.dart @@ -4,6 +4,7 @@ import "package:photos/services/filter/collection_ignore.dart"; import "package:photos/services/filter/dedupe_by_upload_id.dart"; import "package:photos/services/filter/filter.dart"; import "package:photos/services/filter/only_uploaded_files_filter.dart"; +import "package:photos/services/filter/shared.dart"; import "package:photos/services/filter/upload_ignore.dart"; import "package:photos/services/ignored_files_service.dart"; @@ -18,12 +19,16 @@ class DBFilterOptions { bool ignoreSavedFiles; bool onlyUploadedFiles; + // If true, files owned by other users or uploaded by other users will be ignored. + bool ignoreSharedItems = false; + DBFilterOptions({ this.ignoredCollectionIDs, this.hideIgnoredForUpload = false, this.dedupeUploadID = true, this.ignoreSavedFiles = false, this.onlyUploadedFiles = false, + this.ignoreSharedItems = false, }); static DBFilterOptions dedupeOption = DBFilterOptions( @@ -39,6 +44,9 @@ Future> applyDBFilters( return files; } final List filters = []; + if (options.ignoreSharedItems) { + filters.add(SkipSharedFileFilter()); + } if (options.hideIgnoredForUpload) { final Map idToReasonMap = await IgnoredFilesService.instance.idToIgnoreReasonMap; diff --git a/mobile/lib/services/filter/shared.dart b/mobile/lib/services/filter/shared.dart new file mode 100644 index 0000000000..823573ff88 --- /dev/null +++ b/mobile/lib/services/filter/shared.dart @@ -0,0 +1,10 @@ +import "package:photos/models/file/extensions/file_props.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/services/filter/filter.dart"; + +class SkipSharedFileFilter extends Filter { + @override + bool filter(EnteFile file) { + return file.uploaderName == null && file.isOwner; + } +} diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 8e11dfa574..ce23850e36 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -1,29 +1,62 @@ -import "dart:convert"; -import "dart:io"; +import 'dart:convert'; +import 'dart:io'; -import "package:flutter/material.dart"; +import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart' as hw; -import "package:logging/logging.dart"; -import "package:path_provider/path_provider.dart"; -import "package:path_provider_foundation/path_provider_foundation.dart"; -import "package:photos/core/constants.dart"; -import "package:photos/models/file/file.dart"; -import "package:photos/services/memory_home_widget_service.dart"; -import "package:photos/services/smart_memories_service.dart"; -import "package:photos/utils/thumbnail_util.dart"; -import "package:shared_preferences/shared_preferences.dart"; +import 'package:home_widget/home_widget.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'package:photos/core/constants.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/services/album_home_widget_service.dart'; +import 'package:photos/services/memory_home_widget_service.dart'; +import 'package:photos/services/people_home_widget_service.dart'; +import 'package:photos/services/smart_memories_service.dart'; +import 'package:photos/utils/thumbnail_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +enum WidgetStatus { + notSynced, + syncedPartially, + syncedEmpty, + syncedAll, +} + +/// Service to manage home screen widgets across the application +/// Handles widget initialization, updates, and interaction with platform-specific widget APIs class HomeWidgetService { - final Logger _logger = Logger((HomeWidgetService).toString()); + // Constants + static const double THUMBNAIL_SIZE = 512.0; + static const String WIDGET_DIRECTORY = 'home_widget'; - HomeWidgetService._privateConstructor(); + // URI schemes for different widget types + static const String MEMORY_WIDGET_SCHEME = 'memorywidget'; + static const String PEOPLE_WIDGET_SCHEME = 'peoplewidget'; + static const String ALBUM_WIDGET_SCHEME = 'albumwidget'; + + // Query parameter keys + static const String GENERATED_ID_PARAM = 'generatedId'; + static const String MAIN_KEY_PARAM = 'mainKey'; + + // Widget data keys + static const String DATA_SUFFIX = '_data'; static final HomeWidgetService instance = HomeWidgetService._privateConstructor(); + HomeWidgetService._privateConstructor(); - init(SharedPreferences prefs) { - setAppGroupID(iOSGroupID); + final Logger _logger = Logger((HomeWidgetService).toString()); + + void init(SharedPreferences prefs) { + setAppGroupID(iOSGroupIDMemory); + _initializeWidgetServices(prefs); + } + + void _initializeWidgetServices(SharedPreferences prefs) { MemoryHomeWidgetService.instance.init(prefs); + PeopleHomeWidgetService.instance.init(prefs); + AlbumHomeWidgetService.instance.init(prefs); } void setAppGroupID(String id) { @@ -31,7 +64,9 @@ class HomeWidgetService { } Future initHomeWidget() async { - await MemoryHomeWidgetService.instance.initMemoryHW(null); + await MemoryHomeWidgetService.instance.initMemoryHomeWidget(null); + await PeopleHomeWidgetService.instance.initHomeWidget(null); + await AlbumHomeWidgetService.instance.initHomeWidget(null); } Future updateWidget({ @@ -46,114 +81,213 @@ class HomeWidgetService { ); } - Future getData(String key) async => - await hw.HomeWidget.getWidgetData(key); + Future getData(String key) async { + return hw.HomeWidget.getWidgetData(key); + } - Future setData(String key, T? data) async => - await hw.HomeWidget.saveWidgetData(key, data); + Future setData(String key, T? data) async { + return hw.HomeWidget.saveWidgetData(key, data); + } Future renderFile( - EnteFile randomFile, + EnteFile file, String key, String title, + String? mainKey, ) async { - const size = 512.0; - - final result = await _captureFile(randomFile, key, title); + final result = await _captureFile(file, key, title, mainKey); if (!result) { - _logger.warning("can't capture file ${randomFile.displayName}"); + _logger.warning("Failed to capture file ${file.displayName}"); return null; } - return const Size(size, size); + return const Size(THUMBNAIL_SIZE, THUMBNAIL_SIZE); } - Future countHomeWidgets() async { - return (await hw.HomeWidget.getInstalledWidgets()).length; + Future countHomeWidgets( + String androidClass, + String iOSClass, + ) async { + final installedWidgets = await getInstalledWidgets(); + final relevantWidgets = installedWidgets + .where( + (widget) => + (widget.androidClassName?.contains(androidClass) ?? false) || + widget.iOSKind == iOSClass, + ) + .toList(); + + return relevantWidgets.length; + } + + Future> getInstalledWidgets() async { + return await hw.HomeWidget.getInstalledWidgets(); } Future _captureFile( - EnteFile ogFile, + EnteFile file, String key, String title, + String? mainKey, ) async { try { - final thumbnail = await getThumbnail(ogFile); - - late final String? directory; - - // coverage:ignore-start - if (Platform.isIOS) { - final PathProviderFoundation provider = PathProviderFoundation(); - directory = await provider.getContainerPath( - appGroupIdentifier: iOSGroupID, - ); - } else { - directory = (await getApplicationSupportDirectory()).path; + // Get thumbnail data + final thumbnail = await getThumbnail(file); + if (thumbnail == null) { + _logger.warning("Failed to get thumbnail for file ${file.displayName}"); + return false; } - final String path = '$directory/home_widget/$key.png'; - final File file = File(path); - if (!await file.exists()) { - await file.create(recursive: true); + // Get appropriate directory for widget assets + final String widgetDirectory = await _getWidgetStorageDirectory(); + + // Save thumbnail to file + final String thumbnailPath = + '$widgetDirectory/$WIDGET_DIRECTORY/$key.png'; + final File thumbnailFile = File(thumbnailPath); + + if (!await thumbnailFile.exists()) { + await thumbnailFile.create(recursive: true); } - await file.writeAsBytes(thumbnail!); - await setData(key, path); + await thumbnailFile.writeAsBytes(thumbnail); + await setData(key, thumbnailPath); + // Format date for display final subText = await SmartMemoriesService.getDateFormattedLocale( - creationTime: ogFile.creationTime!, + creationTime: file.creationTime!, ); - final data = { + // Create metadata + final Map metadata = { "title": title, "subText": subText, - "generatedId": ogFile.generatedID!, + "generatedId": file.generatedID!, + if (mainKey != null) "mainKey": mainKey, }; - if (Platform.isIOS) { - await hw.HomeWidget.saveWidgetData>( - key + "_data", - data, - ); - } else { - await hw.HomeWidget.saveWidgetData( - key + "_data", - jsonEncode(data), - ); - } - } catch (_, __) { - _logger.severe("Failed to save the capture", _, __); + + // Save metadata in platform-specific format + await _saveWidgetMetadata(key, metadata); + + return true; + } catch (error, stackTrace) { + _logger.severe("Failed to save the thumbnail", error, stackTrace); return false; } - return true; + } + + Future _saveWidgetMetadata( + String key, + Map metadata, + ) async { + final String dataKey = key + DATA_SUFFIX; + + if (Platform.isIOS) { + // iOS can store the map directly + await hw.HomeWidget.saveWidgetData>( + dataKey, + metadata, + ); + } else { + // Android needs the data as a JSON string + await hw.HomeWidget.saveWidgetData( + dataKey, + jsonEncode(metadata), + ); + } + } + + Future _getWidgetStorageDirectory() async { + if (Platform.isIOS) { + final PathProviderFoundation provider = PathProviderFoundation(); + return (await provider.getContainerPath( + appGroupIdentifier: iOSGroupIDMemory, + ))!; + } else { + return (await getApplicationSupportDirectory()).path; + } } Future clearWidget(bool autoLogout) async { if (autoLogout) { - setAppGroupID(iOSGroupID); + setAppGroupID(iOSGroupIDMemory); + } + + await Future.wait([ + MemoryHomeWidgetService.instance.clearWidget(), + PeopleHomeWidgetService.instance.clearWidget(), + AlbumHomeWidgetService.instance.clearWidget(), + ]); + + try { + final String widgetParent = await _getWidgetStorageDirectory(); + final String widgetPath = '$widgetParent/$WIDGET_DIRECTORY'; + final dir = Directory(widgetPath); + + await dir.delete(recursive: true); + _logger.info("Widget directory cleared successfully"); + } catch (e) { + _logger.severe("Failed to clear widget directory", e); } - await MemoryHomeWidgetService.instance.clearWidget(); } + /// Handle app launch from a widget Future onLaunchFromWidget(Uri? uri, BuildContext context) async { if (uri == null) { - _logger.warning("onLaunchFromWidget: uri is null"); + _logger.warning("Widget launch failed: URI is null"); return; } - final generatedId = int.tryParse(uri.queryParameters["generatedId"] ?? ""); - + final generatedId = + int.tryParse(uri.queryParameters[GENERATED_ID_PARAM] ?? ""); if (generatedId == null) { - _logger.warning("onLaunchFromWidget: generatedId is null"); + _logger.warning("Widget launch failed: Invalid or missing generated ID"); return; } - if (uri.scheme == "memorywidget") { - _logger.info("onLaunchFromWidget: redirecting to memory widget"); - await MemoryHomeWidgetService.instance.onLaunchFromWidget( - generatedId, - context, - ); + // Route to appropriate handler based on widget scheme + switch (uri.scheme) { + case MEMORY_WIDGET_SCHEME: + _logger.info("Launching app from memory widget"); + await MemoryHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + context, + ); + break; + + case PEOPLE_WIDGET_SCHEME: + _logger.info("Launching app from people widget"); + final personId = uri.queryParameters[MAIN_KEY_PARAM] ?? ""; + await PeopleHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + personId, + context, + ); + break; + + case ALBUM_WIDGET_SCHEME: + _logger.info("Launching app from album widget"); + final collectionId = + int.tryParse(uri.queryParameters[MAIN_KEY_PARAM] ?? ""); + if (collectionId == null) { + _logger.warning( + "Album widget launch failed: Invalid or missing collection ID", + ); + return; + } + + await AlbumHomeWidgetService.instance.onLaunchFromWidget( + generatedId, + collectionId, + context, + ); + break; + + default: + _logger.warning( + "Widget launch failed: Unknown widget scheme '${uri.scheme}'", + ); + break; } } } diff --git a/mobile/lib/services/isolate_functions.dart b/mobile/lib/services/isolate_functions.dart index 929790c588..ae4e1e8851 100644 --- a/mobile/lib/services/isolate_functions.dart +++ b/mobile/lib/services/isolate_functions.dart @@ -1,4 +1,3 @@ -import "dart:io" show File; import 'dart:typed_data' show Uint8List; import "package:ml_linalg/linalg.dart"; @@ -98,12 +97,11 @@ Future isolateFunction( /// MLComputer case IsolateOperation.generateFaceThumbnails: final imagePath = args['imagePath'] as String; - final Uint8List imageData = await File(imagePath).readAsBytes(); final faceBoxesJson = args['faceBoxesList'] as List>; final List faceBoxes = faceBoxesJson.map((json) => FaceBox.fromJson(json)).toList(); final List results = await generateFaceThumbnailsUsingCanvas( - imageData, + imagePath, faceBoxes, ); return List.from(results); diff --git a/mobile/lib/services/machine_learning/compute_controller.dart b/mobile/lib/services/machine_learning/compute_controller.dart new file mode 100644 index 0000000000..72f9dab1fd --- /dev/null +++ b/mobile/lib/services/machine_learning/compute_controller.dart @@ -0,0 +1,244 @@ +import "dart:async"; +import "dart:io"; + +import "package:battery_info/battery_info_plugin.dart"; +import "package:battery_info/model/android_battery_info.dart"; +import "package:battery_info/model/iso_battery_info.dart"; +import "package:flutter/foundation.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/machine_learning_control_event.dart"; +import "package:thermal/thermal.dart"; + +enum _ComputeRunState { + idle, + runningML, + generatingStream, +} + +class ComputeController { + final _logger = Logger("ComputeController"); + + static const kMaximumTemperatureAndroid = 42; // 42 degree celsius + static const kMinimumBatteryLevel = 20; // 20% + final kDefaultInteractionTimeout = Duration(seconds: Platform.isIOS ? 5 : 15); + static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; + + static final _thermal = Thermal(); + IosBatteryInfo? _iosLastBatteryInfo; + AndroidBatteryInfo? _androidLastBatteryInfo; + ThermalStatus? _lastThermalStatus; + + bool _isDeviceHealthy = true; + bool _isUserInteracting = true; + bool _canRunML = false; + bool mlInteractionOverride = false; + late Timer _userInteractionTimer; + + _ComputeRunState _currentRunState = _ComputeRunState.idle; + bool _waitingToRunML = false; + + bool get isDeviceHealthy => _isDeviceHealthy; + + ComputeController() { + _logger.info('MachineLearningController constructor'); + _startInteractionTimer(kDefaultInteractionTimeout); + if (Platform.isIOS) { + if (kDebugMode) { + _logger.info( + "iOS battery info stream is not available in simulator, disabling in debug mode", + ); + // if you need to test on physical device, uncomment this check + return; + } + BatteryInfoPlugin() + .iosBatteryInfoStream + .listen((IosBatteryInfo? batteryInfo) { + _oniOSBatteryStateUpdate(batteryInfo); + }); + } + if (Platform.isAndroid) { + BatteryInfoPlugin() + .androidBatteryInfoStream + .listen((AndroidBatteryInfo? batteryInfo) { + _onAndroidBatteryStateUpdate(batteryInfo); + }); + } + _thermal.onThermalStatusChanged.listen((ThermalStatus thermalState) { + _onThermalStateUpdate(thermalState); + }); + _logger.info('init done '); + } + + bool requestCompute({bool ml = false, bool stream = false}) { + _logger.info("Requesting compute: ml: $ml, stream: $stream"); + if (!_isDeviceHealthy || !_canRunGivenUserInteraction()) { + _logger.info("Device not healthy or user interacting, denying request."); + return false; + } + if (ml) { + return _requestML(); + } else if (stream) { + return _requestStream(); + } + _logger.severe("No compute request specified, denying request."); + return false; + } + + bool _requestML() { + if (_currentRunState == _ComputeRunState.idle) { + _currentRunState = _ComputeRunState.runningML; + _waitingToRunML = false; + _logger.info("ML request granted"); + return true; + } else if (_currentRunState == _ComputeRunState.runningML) { + return true; + } + _logger.info( + "ML request denied, current state: $_currentRunState, wants to run ML: $_waitingToRunML", + ); + _waitingToRunML = true; + return false; + } + + bool _requestStream() { + if (_currentRunState == _ComputeRunState.idle && !_waitingToRunML) { + _logger.info("Stream request granted"); + _currentRunState = _ComputeRunState.generatingStream; + return true; + } else if (_currentRunState == _ComputeRunState.generatingStream && !_waitingToRunML) { + return true; + } + _logger.info( + "Stream request denied, current state: $_currentRunState, wants to run ML: $_waitingToRunML", + ); + return false; + } + + void releaseCompute({bool ml = false, bool stream = false}) { + _logger.info( + "Releasing compute: ml: $ml, stream: $stream, current state: $_currentRunState", + ); + + if (ml) { + if (_currentRunState == _ComputeRunState.runningML) { + _currentRunState = _ComputeRunState.idle; + } + _waitingToRunML = false; + } else if (stream) { + if (_currentRunState == _ComputeRunState.generatingStream) { + _currentRunState = _ComputeRunState.idle; + } + } + } + + void onUserInteraction() { + if (!_isUserInteracting) { + _logger.info("User is interacting with the app"); + _isUserInteracting = true; + _fireControlEvent(); + } + _resetTimer(); + } + + bool _canRunGivenUserInteraction() { + return !_isUserInteracting || mlInteractionOverride; + } + + void forceOverrideML({required bool turnOn}) { + _logger.info("Forcing to turn on ML: $turnOn"); + mlInteractionOverride = turnOn; + _fireControlEvent(); + } + + void _fireControlEvent() { + final shouldRunML = _isDeviceHealthy && _canRunGivenUserInteraction(); + if (shouldRunML != _canRunML) { + _canRunML = shouldRunML; + _logger.info( + "Firing event: $shouldRunML (device health: $_isDeviceHealthy, user interaction: $_isUserInteracting, mlInteractionOverride: $mlInteractionOverride)", + ); + Bus.instance.fire(MachineLearningControlEvent(shouldRunML)); + } + } + + void _startInteractionTimer(Duration timeout) { + _userInteractionTimer = Timer(timeout, () { + _isUserInteracting = false; + _fireControlEvent(); + }); + } + + void _resetTimer() { + _userInteractionTimer.cancel(); + _startInteractionTimer(kDefaultInteractionTimeout); + } + + void _onAndroidBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) { + _androidLastBatteryInfo = batteryInfo; + _logger.info("Battery info: ${batteryInfo!.toJson()}"); + _isDeviceHealthy = _computeIsAndroidDeviceHealthy(); + _fireControlEvent(); + } + + void _oniOSBatteryStateUpdate(IosBatteryInfo? batteryInfo) { + _iosLastBatteryInfo = batteryInfo; + _logger.info("Battery info: ${batteryInfo!.toJson()}"); + _isDeviceHealthy = _computeIsiOSDeviceHealthy(); + _fireControlEvent(); + } + + void _onThermalStateUpdate(ThermalStatus? thermalStatus) { + _lastThermalStatus = thermalStatus; + _logger.info("Thermal status: $thermalStatus"); + _isDeviceHealthy = _computeIsAndroidDeviceHealthy(); + _fireControlEvent(); + } + + bool _computeIsAndroidDeviceHealthy() { + return _hasSufficientBattery( + _androidLastBatteryInfo?.batteryLevel ?? kMinimumBatteryLevel, + ) && + _isAcceptableTemperatureAndroid() && + _isBatteryHealthyAndroid() && + _isAcceptableThermalState(); + } + + bool _computeIsiOSDeviceHealthy() { + return _hasSufficientBattery( + _iosLastBatteryInfo?.batteryLevel ?? kMinimumBatteryLevel, + ) && + _isAcceptableThermalState(); + } + + bool _isAcceptableThermalState() { + switch (_lastThermalStatus) { + case null: + return true; + case ThermalStatus.none: + case ThermalStatus.light: + case ThermalStatus.moderate: + return true; + case ThermalStatus.severe: + case ThermalStatus.critical: + case ThermalStatus.emergency: + case ThermalStatus.shutdown: + _logger.warning("Thermal status is unacceptable: $_lastThermalStatus"); + return false; + } + } + + bool _hasSufficientBattery(int batteryLevel) { + return batteryLevel >= kMinimumBatteryLevel; + } + + bool _isAcceptableTemperatureAndroid() { + return (_androidLastBatteryInfo?.temperature ?? + kMaximumTemperatureAndroid) <= + kMaximumTemperatureAndroid; + } + + bool _isBatteryHealthyAndroid() { + return !kUnhealthyStates.contains(_androidLastBatteryInfo?.health ?? ""); + } +} diff --git a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart index ce29ccc269..b97573fa60 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart @@ -8,6 +8,9 @@ const kLaplacianVerySoftThreshold = 200; /// Default blur value const kLapacianDefault = 10000.0; +/// The minimum score for a face to be shown as detected in the UI +const kMinimumFaceShowScore = 0.70; + /// The minimum score for a face to be considered a high quality face for clustering and person detection const kMinimumQualityFaceScore = 0.80; const kMediumQualityFaceScore = 0.85; diff --git a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart index 8742970e33..efcf76a62a 100644 --- a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart +++ b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart @@ -57,7 +57,7 @@ class ClusterFeedbackService { /// 1. clusterID: the ID of the cluster /// 2. distance: the distance between the person's cluster and the suggestion /// 3. bool: whether the suggestion was found using the mean (true) or the median (false) - /// 4. List: the files in the cluster + /// 4. `List`: the files in the cluster Future> getSuggestionForPerson( PersonEntity person, { bool extremeFilesFirst = true, diff --git a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart index daa3ddc0ff..7c96a67dea 100644 --- a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart @@ -4,7 +4,6 @@ import "dart:developer"; import "package:flutter/foundation.dart"; import "package:logging/logging.dart"; import "package:photos/core/event_bus.dart"; -import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/extensions/stop_watch.dart"; @@ -14,8 +13,7 @@ import 'package:photos/models/ml/face/face.dart'; import "package:photos/models/ml/face/person.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/entity_service.dart"; -import "package:photos/services/machine_learning/ml_result.dart"; -import "package:photos/services/search_service.dart"; +import "package:photos/utils/face/face_thumbnail_cache.dart"; import "package:shared_preferences/shared_preferences.dart"; class PersonService { @@ -38,6 +36,8 @@ class PersonService { return _instance!; } + static bool get isInitialized => _instance != null; + late Logger logger = Logger("PersonService"); static Future init( @@ -443,6 +443,7 @@ class PersonService { data: person.data.copyWith(avatarFaceId: face.faceID), ); await updatePerson(updatedPerson); + await putFaceIdCachedForPersonOrCluster(p.remoteID, face.faceID); return updatedPerson; } @@ -484,49 +485,4 @@ class PersonService { rethrow; } } - - Future getThumbnailFileOfPerson( - PersonEntity person, - ) async { - try { - if (person.data.hasAvatar()) { - final avatarFileID = tryGetFileIdFromFaceId(person.data.avatarFaceID!); - if (avatarFileID != null) { - final file = (await FilesDB.instance - .getFileIDToFileFromIDs([avatarFileID]))[avatarFileID]; - if (file != null) { - return file; - } else { - logger.severe("Avatar File not found for face ${person.data}"); - } - } - } - - final clustersToFiles = - await SearchService.instance.getClusterFilesForPersonID( - person.remoteID, - ); - - EnteFile? resultFile; - // iterate over all clusters and get the first file - for (final clusterFiles in clustersToFiles.values) { - for (final file in clusterFiles) { - resultFile ??= file; - if (resultFile.creationTime! < file.creationTime!) { - resultFile = file; - } - } - } - if (resultFile == null) { - logger.warning( - "Person ${kDebugMode ? person.data.name : person.remoteID} has no files", - ); - return null; - } - return resultFile; - } catch (e, s) { - logger.severe("Error in getThumbnailFileOfPerson", e, s); - return null; - } - } } diff --git a/mobile/lib/services/machine_learning/machine_learning_controller.dart b/mobile/lib/services/machine_learning/machine_learning_controller.dart deleted file mode 100644 index 43b24dc9a5..0000000000 --- a/mobile/lib/services/machine_learning/machine_learning_controller.dart +++ /dev/null @@ -1,131 +0,0 @@ -import "dart:async"; -import "dart:io"; - -import "package:battery_info/battery_info_plugin.dart"; -import "package:battery_info/model/android_battery_info.dart"; -import "package:battery_info/model/iso_battery_info.dart"; -import "package:flutter/foundation.dart"; -import "package:logging/logging.dart"; -import "package:photos/core/event_bus.dart"; -import "package:photos/events/machine_learning_control_event.dart"; - -class MachineLearningController { - final _logger = Logger("MachineLearningController"); - - static const kMaximumTemperature = 42; // 42 degree celsius - static const kMinimumBatteryLevel = 20; // 20% - final kDefaultInteractionTimeout = Duration(seconds: Platform.isIOS ? 5 : 15); - static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; - - bool _isDeviceHealthy = true; - bool _isUserInteracting = true; - bool _canRunML = false; - bool mlInteractionOverride = false; - late Timer _userInteractionTimer; - - bool get isDeviceHealthy => _isDeviceHealthy; - - MachineLearningController() { - _logger.info('MachineLearningController constructor'); - _startInteractionTimer(kDefaultInteractionTimeout); - if (Platform.isIOS) { - if (kDebugMode) { - _logger.info( - "iOS battery info stream is not available in simulator, disabling in debug mode", - ); - // if you need to test on physical device, uncomment this check - return; - } - BatteryInfoPlugin() - .iosBatteryInfoStream - .listen((IosBatteryInfo? batteryInfo) { - _oniOSBatteryStateUpdate(batteryInfo); - }); - } - if (Platform.isAndroid) { - BatteryInfoPlugin() - .androidBatteryInfoStream - .listen((AndroidBatteryInfo? batteryInfo) { - _onAndroidBatteryStateUpdate(batteryInfo); - }); - } - _logger.info('init done '); - } - - void onUserInteraction() { - if (!_isUserInteracting) { - _logger.info("User is interacting with the app"); - _isUserInteracting = true; - _fireControlEvent(); - } - _resetTimer(); - } - - bool _canRunGivenUserInteraction() { - return !_isUserInteracting || mlInteractionOverride; - } - - void forceOverrideML({required bool turnOn}) { - _logger.info("Forcing to turn on ML: $turnOn"); - mlInteractionOverride = turnOn; - _fireControlEvent(); - } - - void _fireControlEvent() { - final shouldRunML = _isDeviceHealthy && _canRunGivenUserInteraction(); - if (shouldRunML != _canRunML) { - _canRunML = shouldRunML; - _logger.info( - "Firing event: $shouldRunML (device health: $_isDeviceHealthy, user interaction: $_isUserInteracting, mlInteractionOverride: $mlInteractionOverride)", - ); - Bus.instance.fire(MachineLearningControlEvent(shouldRunML)); - } - } - - void _startInteractionTimer(Duration timeout) { - _userInteractionTimer = Timer(timeout, () { - _logger.info("User is not interacting with the app"); - _isUserInteracting = false; - _fireControlEvent(); - }); - } - - void _resetTimer() { - _userInteractionTimer.cancel(); - _startInteractionTimer(kDefaultInteractionTimeout); - } - - void _onAndroidBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) { - _logger.info("Battery info: ${batteryInfo!.toJson()}"); - _isDeviceHealthy = _computeIsAndroidDeviceHealthy(batteryInfo); - _fireControlEvent(); - } - - void _oniOSBatteryStateUpdate(IosBatteryInfo? batteryInfo) { - _logger.info("Battery info: ${batteryInfo!.toJson()}"); - _isDeviceHealthy = _computeIsiOSDeviceHealthy(batteryInfo); - _fireControlEvent(); - } - - bool _computeIsAndroidDeviceHealthy(AndroidBatteryInfo info) { - return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel) && - _isAcceptableTemperature(info.temperature ?? kMaximumTemperature) && - _isBatteryHealthy(info.health ?? ""); - } - - bool _computeIsiOSDeviceHealthy(IosBatteryInfo info) { - return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel); - } - - bool _hasSufficientBattery(int batteryLevel) { - return batteryLevel >= kMinimumBatteryLevel; - } - - bool _isAcceptableTemperature(int temperature) { - return temperature <= kMaximumTemperature; - } - - bool _isBatteryHealthy(String health) { - return !kUnhealthyStates.contains(health); - } -} diff --git a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart index 52da789eb7..635f6120b1 100644 --- a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart +++ b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart @@ -28,6 +28,9 @@ class MLIndexingIsolate extends SuperIsolate { @override bool get shouldAutomaticDispose => true; + int _loadedModelsCount = 0; + int _deloadedModelsCount = 0; + final _initModelLock = Lock(); final _downloadModelLock = Lock(); @@ -108,7 +111,6 @@ class MLIndexingIsolate extends SuperIsolate { } return _downloadModelLock.synchronized(() async { if (areModelsDownloaded) { - _logger.finest("Models already downloaded"); return; } final goodInternet = await canUseHighBandwidth(); @@ -125,6 +127,7 @@ class MLIndexingIsolate extends SuperIsolate { ClipImageEncoder.instance.downloadModel(forceRefresh), ]); areModelsDownloaded = true; + _logger.info('Downloaded models'); }); } @@ -144,6 +147,10 @@ class MLIndexingIsolate extends SuperIsolate { _logger.info( 'Loading models. faces: $shouldLoadFaces, clip: $shouldLoadClip', ); + _loadedModelsCount++; + _logger.info( + "Loaded models count: $_loadedModelsCount, deloaded models count: $_deloadedModelsCount", + ); await MLIndexingIsolate.instance ._loadModels(loadFaces: shouldLoadFaces, loadClip: shouldLoadClip); _logger.info('Models loaded'); @@ -249,6 +256,11 @@ class MLIndexingIsolate extends SuperIsolate { } if (modelNames.isEmpty) return; try { + _logger.info("Releasing models $modelNames"); + _deloadedModelsCount++; + _logger.info( + "Loaded models count: $_loadedModelsCount, deloaded models count: $_deloadedModelsCount", + ); await runInIsolate(IsolateOperation.releaseIndexingModels, { "modelNames": modelNames, "modelAddresses": modelAddresses, diff --git a/mobile/lib/services/machine_learning/ml_service.dart b/mobile/lib/services/machine_learning/ml_service.dart index 0961b50e4c..42561d234d 100644 --- a/mobile/lib/services/machine_learning/ml_service.dart +++ b/mobile/lib/services/machine_learning/ml_service.dart @@ -13,7 +13,6 @@ import "package:photos/events/people_changed_event.dart"; import "package:photos/models/ml/face/face.dart"; import "package:photos/models/ml/ml_versions.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/filedata/model/file_data.dart"; import 'package:photos/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart'; import "package:photos/services/machine_learning/face_ml/face_clustering/face_db_info_for_clustering.dart"; @@ -21,6 +20,7 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; import 'package:photos/services/machine_learning/ml_result.dart'; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; +import "package:photos/services/video_preview_service.dart"; import "package:photos/utils/ml_util.dart"; import "package:photos/utils/network_util.dart"; import "package:photos/utils/ram_check_util.dart"; @@ -51,6 +51,9 @@ class MLService { bool _isRunningML = false; bool _shouldPauseIndexingAndClustering = false; + bool get isRunningML => + _isRunningML || memoriesCacheService.isUpdatingMemories; + static const _kForceClusteringFaceCount = 8000; late final mlDataDB = MLDataDB.instance; @@ -116,7 +119,7 @@ class MLService { } Future sync() async { - await FileDataService.instance.syncFDStatus(); + await fileDataService.syncFDStatus(); await faceRecognitionService.syncPersonFeedback(); } @@ -126,6 +129,7 @@ class MLService { _mlControllerStatus = true; } if (!_canRunMLFunction(function: "AllML") && !force) return; + if (!force && !computeController.requestCompute(ml: true)) return; _isRunningML = true; await sync(); @@ -160,6 +164,8 @@ class MLService { rethrow; } finally { _isRunningML = false; + computeController.releaseCompute(ml: true); + VideoPreviewService.instance.queueFiles(); } } @@ -494,11 +500,10 @@ class MLService { ); } // Storing results on remote - await FileDataService.instance.putFileData( + await fileDataService.putFileData( instruction.file, dataEntity, ); - _logger.info("ML results for fileID ${result.fileId} stored on remote"); // Storing results locally if (result.facesRan) await mlDataDB.bulkInsertFaces(faces); if (result.clipRan) { @@ -506,7 +511,7 @@ class MLService { result.clip!, ); } - _logger.info("ML results for fileID ${result.fileId} stored locally"); + _logger.info("ML result for fileID ${result.fileId} stored remote+local"); return actuallyRanML; } catch (e, s) { final String errorString = e.toString(); diff --git a/mobile/lib/services/magic_cache_service.dart b/mobile/lib/services/magic_cache_service.dart index 462cb851fd..66e970f31d 100644 --- a/mobile/lib/services/magic_cache_service.dart +++ b/mobile/lib/services/magic_cache_service.dart @@ -24,6 +24,7 @@ import "package:photos/services/machine_learning/semantic_search/semantic_search import "package:photos/services/remote_assets_service.dart"; import "package:photos/services/search_service.dart"; import "package:photos/ui/viewer/search/result/magic_result_screen.dart"; +import "package:photos/utils/cache_util.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/navigation_util.dart"; import "package:shared_preferences/shared_preferences.dart"; @@ -251,13 +252,12 @@ class MagicCacheService { final List magicCaches = await _nonEmptyMagicResults(magicPromptsData); w?.log("resultComputed"); - final file = File(await _getCachePath()); - if (!file.existsSync()) { - file.createSync(recursive: true); - } _magicCacheFuture = Future.value(magicCaches); - await file - .writeAsBytes(MagicCache.encodeListToJson(magicCaches).codeUnits); + await writeToJsonFile>( + await _getCachePath(), + magicCaches, + MagicCache.encodeListToJson, + ); w?.log("cacheWritten"); await _resetLastMagicCacheUpdateTime(); w?.logAndReset('done'); @@ -300,20 +300,11 @@ class MagicCacheService { Future> _readResultFromDisk() async { _logger.info("Reading magic cache result from disk"); - final file = File(await _getCachePath()); - if (!file.existsSync()) { - _logger.info("No magic cache found"); - return []; - } - try { - final bytes = await file.readAsBytes(); - final jsonString = String.fromCharCodes(bytes); - return MagicCache.decodeJsonToList(jsonString); - } catch (e, s) { - _logger.severe("Error reading or decoding cache file", e, s); - await file.delete(); - rethrow; - } + final cache = await decodeJsonFile>( + await _getCachePath(), + MagicCache.decodeJsonToList, + ); + return cache ?? []; } Future clearMagicCache() async { diff --git a/mobile/lib/services/memories_cache_service.dart b/mobile/lib/services/memories_cache_service.dart index f324d7bdea..37f5eebf03 100644 --- a/mobile/lib/services/memories_cache_service.dart +++ b/mobile/lib/services/memories_cache_service.dart @@ -6,6 +6,7 @@ import "package:flutter/material.dart" show BuildContext; import "package:logging/logging.dart"; import "package:path_provider/path_provider.dart"; import "package:photos/core/event_bus.dart"; +import "package:photos/db/files_db.dart"; import "package:photos/db/memories_db.dart"; import "package:photos/events/files_updated_event.dart"; import "package:photos/events/memories_changed_event.dart"; @@ -15,11 +16,17 @@ import "package:photos/extensions/stop_watch.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/people_memory.dart"; import "package:photos/models/memories/smart_memory.dart"; import "package:photos/models/memories/smart_memory_constants.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/language_service.dart"; +import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; +import "package:photos/services/notification_service.dart"; import "package:photos/services/search_service.dart"; import "package:photos/ui/home/memories/full_screen_memory.dart"; +import "package:photos/ui/viewer/people/people_page.dart"; +import "package:photos/utils/cache_util.dart"; import "package:photos/utils/navigation_util.dart"; import "package:shared_preferences/shared_preferences.dart"; import "package:synchronized/synchronized.dart"; @@ -34,14 +41,18 @@ class MemoriesCacheService { static const _kCacheUpdateDelay = Duration(seconds: 5); final SharedPreferences _prefs; - late final Logger _logger = Logger("MemoriesCacheService"); + static final Logger _logger = Logger("MemoriesCacheService"); final _memoriesDB = MemoriesDB.instance; List? _cachedMemories; bool _shouldUpdate = false; + bool _isUpdatingMemories = false; + bool get isUpdatingMemories => _isUpdatingMemories; + final _memoriesUpdateLock = Lock(); + final _memoriesGetLock = Lock(); MemoriesCacheService(this._prefs) { _logger.fine("MemoriesCacheService constructor"); @@ -70,13 +81,6 @@ class MemoriesCacheService { }); } - Future _resetLastMemoriesCacheUpdateTime() async { - await _prefs.setInt( - _lastMemoriesCacheUpdateTimeKey, - DateTime.now().microsecondsSinceEpoch, - ); - } - int get lastMemoriesCacheUpdateTime { return _prefs.getInt(_lastMemoriesCacheUpdateTimeKey) ?? 0; } @@ -117,11 +121,6 @@ class MemoriesCacheService { .microsecondsSinceEpoch; } - Future _getCachePath() async { - return (await getApplicationSupportDirectory()).path + - "/cache/memories_cache"; - } - Future markMemoryAsSeen(Memory memory, bool lastInList) async { memory.markSeen(); await _memoriesDB.markMemoryAsSeen( @@ -146,11 +145,122 @@ class MemoriesCacheService { unawaited(_prefs.setBool(_shouldUpdateCacheKey, true)); } - Future _cacheUpdated() async { - _shouldUpdate = false; - unawaited(_prefs.setBool(_shouldUpdateCacheKey, false)); - await _resetLastMemoriesCacheUpdateTime(); - Bus.instance.fire(MemoriesChangedEvent()); + Future> getMemories() async { + _logger.info("getMemories called"); + if (!showAnyMemories) { + _logger.info('Showing memories is disabled in settings, showing none'); + return []; + } + return _memoriesGetLock.synchronized(() async { + if (_cachedMemories != null && _cachedMemories!.isNotEmpty) { + _logger.info("Found memories in memory cache"); + return _cachedMemories!; + } + try { + if (!enableSmartMemories) { + await _calculateRegularFillers(); + return _cachedMemories!; + } + _cachedMemories = await _getMemoriesFromCache(); + if (_cachedMemories == null || _cachedMemories!.isEmpty) { + _logger.warning( + "No memories found in cache, force updating cache. Possible severe caching issue", + ); + await updateCache(forced: true); + } else { + _logger.info("Found memories in cache"); + } + if (_cachedMemories == null || _cachedMemories!.isEmpty) { + _logger + .severe("No memories found in (computed) cache, getting fillers"); + await _calculateRegularFillers(); + } + return _cachedMemories!; + } catch (e, s) { + _logger.severe("Error in getMemories", e, s); + return []; + } + }); + } + + Future _calculateRegularFillers() async { + if (_cachedMemories == null) { + _cachedMemories = await smartMemoriesService.calcSimpleMemories(); + Bus.instance.fire(MemoriesChangedEvent()); + } + return; + } + + Future?> _getMemoriesFromCache() async { + final cache = await _readCacheFromDisk(); + if (cache == null) { + return null; + } + final result = await fromCacheToMemories(cache); + return result; + } + + Future _readCacheFromDisk() async { + _logger.info("Reading memories cache result from disk"); + final cache = decodeJsonFile( + await _getCachePath(), + MemoriesCache.decodeFromJsonString, + ); + return cache; + } + + static Future> fromCacheToMemories( + MemoriesCache cache, + ) async { + try { + _logger.info('Processing disk cache memories to smart memories'); + final List memories = []; + final seenTimes = await MemoriesDB.instance.getSeenTimes(); + final minimalFileIDs = {}; + for (final ToShowMemory memory in cache.toShowMemories) { + if (memory.shouldShowNow()) { + minimalFileIDs.addAll(memory.fileUploadedIDs); + } + } + final minimalFiles = await FilesDB.instance.getFilesFromIDs( + minimalFileIDs.toList(), + collectionsToIgnore: SearchService.instance.ignoreCollections(), + ); + final minimalFileIdsToFile = {}; + for (final file in minimalFiles) { + if (file.uploadedFileID != null) { + minimalFileIdsToFile[file.uploadedFileID!] = file; + } + } + + for (final ToShowMemory memory in cache.toShowMemories) { + if (memory.shouldShowNow()) { + final smartMemory = SmartMemory( + memory.fileUploadedIDs + .where((fileID) => minimalFileIdsToFile.containsKey(fileID)) + .map( + (fileID) => + Memory.fromFile(minimalFileIdsToFile[fileID]!, seenTimes), + ) + .toList(), + memory.type, + memory.title, + memory.firstTimeToShow, + memory.lastTimeToShow, + id: memory.id, + ); + if (smartMemory.memories.isNotEmpty) { + memories.add(smartMemory); + } + } + } + locationService.baseLocations = cache.baseLocations; + _logger.info('Processing of disk cache memories done'); + return memories; + } catch (e, s) { + _logger.severe("Error converting cache to memories", e, s); + return []; + } } Future updateCache({bool forced = false}) async { @@ -168,10 +278,12 @@ class MemoriesCacheService { _logger.info( "No update needed (shouldUpdate: $_shouldUpdate, forced: $forced)", ); + return; } _logger.info( "Updating memories cache (shouldUpdate: $_shouldUpdate, forced: $forced)", ); + _isUpdatingMemories = true; try { final EnteWatch? w = kDebugMode ? EnteWatch("MemoriesCacheService") : null; @@ -184,7 +296,7 @@ class MemoriesCacheService { final now = DateTime.now(); final next = now.add(kMemoriesUpdateFrequency); final nowResult = - await smartMemoriesService.calcMemories(now, newCache); + await smartMemoriesService.calcSmartMemories(now, newCache); if (nowResult.isEmpty) { _cachedMemories = []; _logger.warning( @@ -193,7 +305,7 @@ class MemoriesCacheService { return; } final nextResult = - await smartMemoriesService.calcMemories(next, newCache); + await smartMemoriesService.calcSmartMemories(next, newCache); w?.log("calculated new memories"); for (final nowMemory in nowResult.memories) { newCache.toShowMemories @@ -205,26 +317,44 @@ class MemoriesCacheService { } newCache.baseLocations.addAll(nowResult.baseLocations); w?.log("added memories to cache"); - final file = File(await _getCachePath()); - if (!file.existsSync()) { - file.createSync(recursive: true); - } _cachedMemories = nowResult.memories .where((memory) => memory.shouldShowNow()) .toList(); + await _scheduleMemoryNotifications( + [...nowResult.memories, ...nextResult.memories], + ); locationService.baseLocations = nowResult.baseLocations; - await file.writeAsBytes( - MemoriesCache.encodeToJsonString(newCache).codeUnits, + await writeToJsonFile( + await _getCachePath(), + newCache, + MemoriesCache.encodeToJsonString, ); w?.log("cacheWritten"); await _cacheUpdated(); w?.logAndReset('_cacheUpdated method done'); } catch (e, s) { _logger.info("Error updating memories cache", e, s); + } finally { + _isUpdatingMemories = false; } }); } + Future _getCachePath() async { + return (await getApplicationSupportDirectory()).path + + "/cache/memories_cache"; + } + + Future _cacheUpdated() async { + _shouldUpdate = false; + unawaited(_prefs.setBool(_shouldUpdateCacheKey, false)); + await _prefs.setInt( + _lastMemoriesCacheUpdateTimeKey, + DateTime.now().microsecondsSinceEpoch, + ); + Bus.instance.fire(MemoriesChangedEvent()); + } + /// WARNING: Use for testing only, TODO: lau: remove later Future debugCacheForTesting() async { final oldCache = await _readCacheFromDisk(); @@ -283,98 +413,44 @@ class MemoriesCacheService { ); } - Future> _fromCacheToMemories(MemoriesCache cache) async { - try { - _logger.info('Processing disk cache memories to smart memories'); - final List memories = []; - final allFiles = Set.from( - await SearchService.instance.getAllFilesForSearch(), + Future clearMemoriesCache({bool fromDisk = true}) async { + if (fromDisk) { + final file = File(await _getCachePath()); + if (file.existsSync()) { + await file.delete(); + } + } + _cachedMemories = null; + } + + Future> getMemoriesForWidget({ + required bool onThisDay, + required bool pastYears, + required bool smart, + }) async { + if (!onThisDay && !pastYears && !smart) { + _logger.info( + 'No memories requested, returning empty list', ); - final allFileIdsToFile = {}; - for (final file in allFiles) { - if (file.uploadedFileID != null) { - allFileIdsToFile[file.uploadedFileID!] = file; - } - } - final seenTimes = await _memoriesDB.getSeenTimes(); - - for (final ToShowMemory memory in cache.toShowMemories) { - if (memory.shouldShowNow()) { - final smartMemory = SmartMemory( - memory.fileUploadedIDs - .where((fileID) => allFileIdsToFile.containsKey(fileID)) - .map( - (fileID) => - Memory.fromFile(allFileIdsToFile[fileID]!, seenTimes), - ) - .toList(), - memory.type, - memory.title, - memory.firstTimeToShow, - memory.lastTimeToShow, - ); - if (smartMemory.memories.isNotEmpty) { - memories.add(smartMemory); - } - } - } - locationService.baseLocations = cache.baseLocations; - _logger.info('Processing of disk cache memories done'); - return memories; - } catch (e, s) { - _logger.severe("Error converting cache to memories", e, s); return []; } - } - - Future?> _getMemoriesFromCache() async { - final cache = await _readCacheFromDisk(); - if (cache == null) { - return null; + final allMemories = await getMemories(); + if (onThisDay && pastYears && smart) { + return allMemories; } - final result = await _fromCacheToMemories(cache); - return result; - } - - Future _calculateRegularFillers() async { - if (_cachedMemories == null) { - _cachedMemories = await smartMemoriesService.calcFillerResults(); - Bus.instance.fire(MemoriesChangedEvent()); - } - return; - } - - Future> getMemories() async { - if (!showAnyMemories) { - _logger.info('Showing memories is disabled in settings, showing none'); - return []; - } - if (_cachedMemories != null && _cachedMemories!.isNotEmpty) { - return _cachedMemories!; - } - try { - if (!enableSmartMemories) { - await _calculateRegularFillers(); - return _cachedMemories!; + final filteredMemories = []; + for (final memory in allMemories) { + if (!memory.shouldShowNow()) continue; + if (memory.type == MemoryType.onThisDay) { + if (!onThisDay) continue; + } else if (memory.type == MemoryType.filler) { + if (!pastYears) continue; + } else { + if (!smart) continue; } - _cachedMemories = await _getMemoriesFromCache(); - if (_cachedMemories == null || _cachedMemories!.isEmpty) { - await updateCache(forced: true); - } - if (_cachedMemories == null || _cachedMemories!.isEmpty) { - _logger - .severe("No memories found in (computed) cache, getting fillers"); - await _calculateRegularFillers(); - } - return _cachedMemories!; - } catch (e, s) { - _logger.severe("Error in getMemories", e, s); - return []; + filteredMemories.add(memory); } - } - - Future?> getCachedMemories() async { - return _cachedMemories; + return filteredMemories; } Future goToMemoryFromGeneratedFileID( @@ -415,43 +491,253 @@ class MemoriesCacheService { ); } - Future _readCacheFromDisk() async { - _logger.info("Reading memories cache result from disk"); - final file = File(await _getCachePath()); - if (!file.existsSync()) { - _logger.info("No memories cache found"); - return null; + Future goToOnThisDayMemory(BuildContext context) async { + final allMemories = await getMemories(); + if (allMemories.isEmpty) return; + int memoryIdx = 0; + bool found = false; + memoryLoop: + for (final memory in allMemories) { + if (memory.type == MemoryType.onThisDay) { + found = true; + break memoryLoop; + } + memoryIdx++; } - final allFiles = Set.from( - await SearchService.instance.getAllFilesForSearch(), + if (!found) { + _logger.warning( + "Could not find onThisDay memory", + ); + return; + } + await routeToPage( + context, + FullScreenMemoryDataUpdater( + initialIndex: 0, + memories: allMemories[memoryIdx].memories, + child: FullScreenMemory(allMemories[memoryIdx].title, 0), + ), + forceCustomPageRoute: true, ); - final allFileIdsToFile = {}; - for (final file in allFiles) { - if (file.uploadedFileID != null) { - allFileIdsToFile[file.uploadedFileID!] = file; + } + + Future goToPersonMemory(BuildContext context, String personID) async { + _logger.info("Going to person memory for personID: $personID"); + final allMemories = await getMemories(); + if (allMemories.isEmpty) return; + final personMemories = []; + for (final memory in allMemories) { + if (memory is PeopleMemory && + (memory.isBirthday ?? false) && + memory.personID == personID) { + personMemories.add(memory); } } - try { - final bytes = await file.readAsBytes(); - final jsonString = String.fromCharCodes(bytes); - final cache = - MemoriesCache.decodeFromJsonString(jsonString, allFileIdsToFile); - _logger.info("Reading memories cache result from disk done"); - return cache; - } catch (e, s) { - _logger.severe("Error reading or decoding cache file", e, s); - await file.delete(); - return null; + PeopleMemory? personMemory; + for (final memory in personMemories) { + if (memory.peopleMemoryType == PeopleMemoryType.youAndThem) { + personMemory = memory; + break; // breaking to prefer youAndThem over spotlight + } + if (memory.peopleMemoryType == PeopleMemoryType.spotlight) { + personMemory = memory; + } + } + + if (personMemory == null) { + _logger.severe( + "Could not find person memory, routing to person page instead", + ); + final person = await PersonService.instance.getPerson(personID); + if (person == null) { + _logger.severe("Person with ID $personID not found"); + return; + } + await routeToPage( + context, + PeoplePage( + person: person, + searchResult: null, + ), + forceCustomPageRoute: true, + ); + } + await routeToPage( + context, + FullScreenMemoryDataUpdater( + initialIndex: 0, + memories: personMemory!.memories, + child: FullScreenMemory(personMemory.title, 0), + ), + forceCustomPageRoute: true, + ); + } + + Future toggleOnThisDayNotifications() async { + final oldValue = localSettings.isOnThisDayNotificationsEnabled; + await localSettings.setOnThisDayNotificationsEnabled(!oldValue); + _logger.info("Turning onThisDayNotifications ${oldValue ? "off" : "on"}"); + if (oldValue) { + await _clearAllScheduledOnThisDayNotifications(); + } else { + queueUpdateCache(); } } - Future clearMemoriesCache({bool fromDisk = true}) async { - if (fromDisk) { - final file = File(await _getCachePath()); - if (file.existsSync()) { - await file.delete(); + Future toggleBirthdayNotifications() async { + final oldValue = localSettings.birthdayNotificationsEnabled; + await localSettings.setBirthdayNotificationsEnabled(!oldValue); + _logger.info("Turning birhtdayNotifications ${oldValue ? "off" : "on"}"); + if (oldValue) { + await _clearAllScheduledBirthdayNotifications(); + } else { + queueUpdateCache(); + } + } + + Future _scheduleMemoryNotifications( + List allMemories, + ) async { + await _scheduleOnThisDayNotifications(allMemories); + await _scheduleBirthdayNotifications(allMemories); + } + + Future _scheduleOnThisDayNotifications( + List allMemories, + ) async { + if (!localSettings.isOnThisDayNotificationsEnabled) { + _logger + .info("On this day notifications are disabled, skipping scheduling"); + return; + } + await _clearAllScheduledOnThisDayNotifications(); + final scheduledDates = {}; + for (final memory in allMemories) { + if (memory.type != MemoryType.onThisDay) { + continue; + } + final numberOfMemories = memory.memories.length; + if (numberOfMemories < 5) continue; + final firstDateToShow = + DateTime.fromMicrosecondsSinceEpoch(memory.firstDateToShow); + final scheduleTime = DateTime( + firstDateToShow.year, + firstDateToShow.month, + firstDateToShow.day, + 8, + ); + if (scheduleTime.isBefore(DateTime.now())) { + _logger.info( + "Skipping scheduling notification for memory ${memory.id} because the date is in the past (date: $scheduleTime)", + ); + continue; + } + if (scheduledDates.contains(scheduleTime)) { + _logger.info( + "Skipping scheduling notification for memory ${memory.id} because the date is already scheduled (date: $scheduleTime)", + ); + continue; + } + final s = await LanguageService.s; + await NotificationService.instance.scheduleNotification( + s.onThisDay, + message: s.lookBackOnYourMemories, + id: memory.id.hashCode, + channelID: "onThisDay", + channelName: s.onThisDay, + payload: memory.id, + dateTime: scheduleTime, + timeoutDurationAndroid: const Duration(hours: 16), + ); + scheduledDates.add(scheduleTime); + _logger.info( + "Scheduled notification for memory ${memory.id} on date: $scheduleTime", + ); + } + } + + Future _scheduleBirthdayNotifications( + List allMemories, + ) async { + if (!localSettings.birthdayNotificationsEnabled) { + _logger.info("birthday notifications are disabled, skipping scheduling"); + return; + } + await _clearAllScheduledBirthdayNotifications(); + final scheduledPersons = {}; + final toSchedule = []; + final peopleToBirthdayMemories = >{}; + for (final memory in allMemories) { + if (memory is PeopleMemory && (memory.isBirthday ?? false)) { + peopleToBirthdayMemories + .putIfAbsent(memory.personID, () => []) + .add(memory); } } - _cachedMemories = null; + personLoop: + for (final personID in peopleToBirthdayMemories.keys) { + final birthdayMemories = peopleToBirthdayMemories[personID]!; + for (final memory in birthdayMemories) { + if (memory.peopleMemoryType == PeopleMemoryType.youAndThem) { + toSchedule.add(memory); + continue personLoop; + } + } + for (final memory in birthdayMemories) { + if (memory.peopleMemoryType == PeopleMemoryType.spotlight) { + toSchedule.add(memory); + continue personLoop; + } + } + } + for (final memory in toSchedule) { + final firstDateToShow = + DateTime.fromMicrosecondsSinceEpoch(memory.firstDateToShow); + final scheduleTime = DateTime( + firstDateToShow.year, + firstDateToShow.month, + firstDateToShow.day, + ); + if (scheduleTime.isBefore(DateTime.now())) { + _logger.info( + "Skipping scheduling notification for memory ${memory.id} because the date is in the past", + ); + continue; + } + if (scheduledPersons.contains(memory.personID)) { + _logger.severe( + "Skipping scheduling notification for memory ${memory.id} because the person's birthday is already scheduled", + ); + continue; + } + final s = await LanguageService.s; + await NotificationService.instance.scheduleNotification( + memory.personName != null + ? s.happyBirthdayToPerson(memory.personName!) + : s.happyBirthday, + id: memory.id.hashCode, + channelID: "birthday", + channelName: s.birthdays, + payload: "birthday_${memory.personID}", + dateTime: scheduleTime, + timeoutDurationAndroid: const Duration(hours: 24), + ); + scheduledPersons.add(memory.personID); + _logger.info( + "Scheduled birthday notification for person ${memory.personID} on date: $scheduleTime", + ); + } + } + + Future _clearAllScheduledOnThisDayNotifications() async { + _logger.info('Clearing all scheduled On This Day notifications'); + await NotificationService.instance + .clearAllScheduledNotifications(containingPayload: "onThisDay"); + } + + Future _clearAllScheduledBirthdayNotifications() async { + _logger.info('Clearing all scheduled birthday notifications'); + await NotificationService.instance + .clearAllScheduledNotifications(containingPayload: "birthday"); } } diff --git a/mobile/lib/services/memory_home_widget_service.dart b/mobile/lib/services/memory_home_widget_service.dart index 6b76bff295..c015dd0a69 100644 --- a/mobile/lib/services/memory_home_widget_service.dart +++ b/mobile/lib/services/memory_home_widget_service.dart @@ -1,164 +1,284 @@ -import "package:flutter/material.dart"; -import "package:fluttertoast/fluttertoast.dart"; -import "package:logging/logging.dart"; -import "package:photos/models/file/file.dart"; -import "package:photos/service_locator.dart"; -import "package:photos/services/home_widget_service.dart"; -import "package:photos/services/sync/local_sync_service.dart"; -import "package:shared_preferences/shared_preferences.dart"; -import "package:synchronized/synchronized.dart"; +import 'dart:math'; +import "package:collection/collection.dart"; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/models/file/file.dart'; +import 'package:photos/models/memories/smart_memory.dart'; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; class MemoryHomeWidgetService { - final Logger _logger = Logger((MemoryHomeWidgetService).toString()); - - MemoryHomeWidgetService._privateConstructor(); + // Constants + static const String SELECTED_LAST_YEAR_MEMORIES_KEY = + "selectedLastYearMemoriesHW"; + static const String SELECTED_ML_MEMORIES_KEY = "selectedMLMemoriesHW"; + static const String SELECTED_ON_THIS_DAY_MEMORIES_KEY = + "selectedOnThisDayMemoriesHW"; + static const String ANDROID_CLASS_NAME = "EnteMemoryWidgetProvider"; + static const String IOS_CLASS_NAME = "EnteMemoryWidget"; + static const String MEMORY_STATUS_KEY = "memoryStatusKey.widget"; + static const String MEMORY_CHANGED_KEY = "memoryChanged.widget"; + static const String TOTAL_MEMORIES_KEY = "totalMemories"; + static const int MAX_MEMORIES_LIMIT = 50; + // Singleton pattern static final MemoryHomeWidgetService instance = MemoryHomeWidgetService._privateConstructor(); + MemoryHomeWidgetService._privateConstructor(); + // Properties + final Logger _logger = Logger((MemoryHomeWidgetService).toString()); late final SharedPreferences _prefs; - final _memoryForceRefreshLock = Lock(); bool _hasSyncedMemory = false; - static const memoryChangedKey = "memoryChanged.widget"; - static const totalMemories = "totalMemories"; - - init(SharedPreferences prefs) { + // Initialization + void init(SharedPreferences prefs) { _prefs = prefs; } - Future _forceMemoryUpdate() async { - await _lockAndLoadMemories(); - await updateMemoryChanged(false); + // Preference getters and setters + Future getSelectedLastYearMemories() async { + return _prefs.getBool(SELECTED_LAST_YEAR_MEMORIES_KEY); } - Future _memorySync() async { - final homeWidgetCount = await HomeWidgetService.instance.countHomeWidgets(); - if (homeWidgetCount == 0) { - _logger.warning("no home widget active"); + Future setSelectedLastYearMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_LAST_YEAR_MEMORIES_KEY, selectedMemories); + } + + Future getSelectedMLMemories() async { + return _prefs.getBool(SELECTED_ML_MEMORIES_KEY); + } + + Future setSelectedMLMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_ML_MEMORIES_KEY, selectedMemories); + } + + Future getSelectedOnThisDayMemories() async { + return _prefs.getBool(SELECTED_ON_THIS_DAY_MEMORIES_KEY); + } + + Future setSelectedOnThisDayMemories(bool selectedMemories) async { + await _prefs.setBool(SELECTED_ON_THIS_DAY_MEMORIES_KEY, selectedMemories); + } + + // Public methods + Future initMemoryHomeWidget(bool? forceFetchNewMemories) async { + if (await _hasAnyBlockers()) { + await clearWidget(); return; } - await _updateWidget(text: "refreshing from same set"); + await _memoryForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + return; + } + + final isWidgetEmpty = await _isWidgetEmpty(); + forceFetchNewMemories ??= await _shouldForceFetchMemories(isWidgetEmpty); + + _logger.warning( + "Initializing memory widget: forceFetch: $forceFetchNewMemories, isEmpty: $isWidgetEmpty", + ); + + if (forceFetchNewMemories!) { + await _forceMemoryUpdate(); + } else if (!isWidgetEmpty) { + await _syncExistingMemories(); + } + }); } - Future hasAnyBlockers() async { + Future clearWidget() async { + final isWidgetEmpty = await _isWidgetEmpty(); + if (isWidgetEmpty) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing MemoryHomeWidget"); + await _setTotalMemories(null); + _hasSyncedMemory = false; + await updateMemoriesStatus(WidgetStatus.syncedEmpty); + await _refreshWidget(message: "MemoryHomeWidget cleared & updated"); + } + + Future updateMemoryChanged(bool value) async { + _logger.info("Updating memory changed flag to $value"); + await _prefs.setBool(MEMORY_CHANGED_KEY, value); + } + + WidgetStatus getMemoriesStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(MEMORY_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updateMemoriesStatus(WidgetStatus value) async { + await _prefs.setInt(MEMORY_STATUS_KEY, value.index); + } + + Future checkPendingMemorySync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isWidgetEmpty = await _isWidgetEmpty(); + final shouldForceFetch = await _shouldForceFetchMemories(isWidgetEmpty); + + if (_hasSyncedMemory && !shouldForceFetch) { + _logger.info("Memories already synced, no action needed"); + return; + } + + await initMemoryHomeWidget(shouldForceFetch); + } + + Future memoryChanged() async { + await updateMemoryChanged(true); + + final cachedMemories = await _getMemoriesForWidget(); + final currentTotal = cachedMemories.length; + final existingTotal = await _getTotalMemories() ?? 0; + + if (existingTotal == currentTotal && existingTotal == 0) { + await updateMemoryChanged(false); + _logger.info("Memories empty, no update needed"); + return; + } + + _logger.info("Memories changed, updating widget"); + await initMemoryHomeWidget(true); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future onLaunchFromWidget(int generatedId, BuildContext context) async { + _hasSyncedMemory = true; + await _syncExistingMemories(); + + await memoriesCacheService.goToMemoryFromGeneratedFileID( + context, + generatedId, + ); + } + + // Private methods + Future _hasAnyBlockers() async { + // Check if first import is completed final hasCompletedFirstImport = LocalSyncService.instance.hasCompletedFirstImport(); if (!hasCompletedFirstImport) { - _logger.warning("first import not completed"); + _logger.warning("First import not completed"); return true; } + // Check if memories are enabled final areMemoriesShown = memoriesCacheService.showAnyMemories; if (!areMemoriesShown) { - _logger.warning("memories not enabled"); + _logger.warning("Memories not enabled"); return true; } return false; } - Future initMemoryHW(bool? forceFetchNewMemories) async { - final result = await hasAnyBlockers(); - if (result) { - await clearWidget(); + Future _forceMemoryUpdate() async { + await _loadAndRenderMemories(); + await updateMemoryChanged(false); + } + + Future _syncExistingMemories() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); return; } - await _memoryForceRefreshLock.synchronized(() async { - final result = await hasAnyBlockers(); - if (result) { - return; - } - final isTotalEmpty = await _checkIfTotalEmpty(); - forceFetchNewMemories ??= await getForceFetchCondition(isTotalEmpty); - - _logger.warning( - "init memory hw: forceFetch: $forceFetchNewMemories, isTotalEmpty: $isTotalEmpty", - ); - - if (forceFetchNewMemories!) { - await _forceMemoryUpdate(); - } else if (!isTotalEmpty) { - await _memorySync(); - } - }); + await _refreshWidget(message: "Refreshing from existing memory set"); } - Future clearWidget() async { - final isTotalEmpty = await _checkIfTotalEmpty(); - if (isTotalEmpty) { - _logger.info(">>> Nothing to clear"); - return; - } - - _logger.info("Clearing MemoryHomeWidget"); - - await _setTotal(null); - _hasSyncedMemory = false; - - await _updateWidget(text: "MemoryHomeWidget cleared & updated"); - } - - Future updateMemoryChanged(bool value) async { - _logger.info("Updating memory changed to $value"); - await _prefs.setBool(memoryChangedKey, value); - } - - Future _checkIfTotalEmpty() async { - final total = await _getTotal(); + Future _isWidgetEmpty() async { + final total = await _getTotalMemories(); return total == 0 || total == null; } - Future getForceFetchCondition(bool isTotalEmpty) async { - final memoryChanged = _prefs.getBool(memoryChangedKey); - if (memoryChanged == true) return true; - - final cachedMemories = await memoriesCacheService.getCachedMemories(); - - final forceFetchNewMemories = - isTotalEmpty && (cachedMemories?.isNotEmpty ?? false); - return forceFetchNewMemories; - } - - Future checkPendingMemorySync() async { - await Future.delayed(const Duration(seconds: 5), () {}); - - final isTotalEmpty = await _checkIfTotalEmpty(); - final forceFetchNewMemories = await getForceFetchCondition(isTotalEmpty); - - if (_hasSyncedMemory && !forceFetchNewMemories) { - _logger.info(">>> Memory already synced"); - return; + Future _shouldForceFetchMemories(bool isWidgetEmpty) async { + // Check if memory changed flag is set + final memoryChanged = _prefs.getBool(MEMORY_CHANGED_KEY) ?? true; + if (memoryChanged == true) { + return true; + } + + final memoriesStatus = getMemoriesStatus(); + switch (memoriesStatus) { + case WidgetStatus.notSynced: + return true; + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; } - await HomeWidgetService.instance.initHomeWidget(); } - Future>> _getMemories() async { - final memories = await memoriesCacheService.getMemories(); + Future> _getMemoriesForWidget() async { + final lastYearValue = await getSelectedLastYearMemories(); + final mlValue = await getSelectedMLMemories(); + final onThisDayValue = await getSelectedOnThisDayMemories(); + final isMLEnabled = flagService.hasGrantedMLConsent; + + final memories = await memoriesCacheService.getMemoriesForWidget( + onThisDay: onThisDayValue ?? !isMLEnabled, + pastYears: lastYearValue ?? !isMLEnabled, + smart: mlValue ?? isMLEnabled, + ); + + return memories; + } + + Future>> _getMemoriesWithFiles() async { + final memories = await _getMemoriesForWidget(); + if (memories.isEmpty) { return {}; } - final files = Map.fromEntries( - memories.map((m) { - return MapEntry(m.title, m.memories.map((e) => e.file).toList()); - }), + return Map.fromEntries( + memories.map( + (memory) => + MapEntry(memory.title, memory.memories.map((m) => m.file).toList()), + ), ); - - return files; } - Future _updateWidget({String? text}) async { + Future _getTotalMemories() async { + return HomeWidgetService.instance.getData(TOTAL_MEMORIES_KEY); + } + + Future _setTotalMemories(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_MEMORIES_KEY, total); + } + + Future _refreshWidget({String? message}) async { await HomeWidgetService.instance.updateWidget( - androidClass: "EnteMemoryWidgetProvider", - iOSClass: "EnteMemoryWidget", + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, ); + if (flagService.internalUser) { await Fluttertoast.showToast( - msg: "[i] ${text ?? "MemoryHomeWidget updated"}", + msg: "[i][mem] ${message ?? "MemoryHomeWidget updated"}", toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, timeInSecForIosWeb: 1, @@ -167,98 +287,97 @@ class MemoryHomeWidgetService { fontSize: 16.0, ); } - _logger.info(">>> Home Widget updated, type: ${text ?? "normal"}"); + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); } - Future memoryChanged() async { - final cachedMemories = await memoriesCacheService.getCachedMemories(); - final currentTotal = cachedMemories?.length ?? 0; + Future _loadAndRenderMemories() async { + final memoriesWithFiles = await _getMemoriesWithFiles(); - final int total = await _getTotal() ?? 0; - - if (total == currentTotal && total == 0) { - _logger.info(">>> Memories not changed, doing nothing"); - return; - } - - _logger.info(">>> Memories changed, updating widget"); - await updateMemoryChanged(true); - await initMemoryHW(true); - } - - Future _getTotal() async { - return HomeWidgetService.instance.getData(totalMemories); - } - - Future _setTotal(int? total) async => - await HomeWidgetService.instance.setData(totalMemories, total); - - Future _lockAndLoadMemories() async { - final files = await _getMemories(); - - if (files.isEmpty) { - _logger.warning("No files found, clearing everything"); + if (memoriesWithFiles.isEmpty) { + _logger.warning("No memories found, clearing widget"); await clearWidget(); return; } - final total = await _getTotal(); - _logger.info(">>> Total memories before: $total"); + final currentTotal = await _getTotalMemories(); + _logger.info("Current total memories in widget: $currentTotal"); - int index = 0; + final bool isWidgetPresent = await countHomeWidgets() > 0; - for (final i in files.entries) { - for (final file in i.value) { - final value = await HomeWidgetService.instance - .renderFile(file, "memory_widget_$index", i.key) - .catchError( - (e, sT) { - _logger.severe("Error rendering widget", e, sT); - return null; - }, - ); + final limit = isWidgetPresent ? MAX_MEMORIES_LIMIT : 5; + final maxAttempts = limit * 10; - if (value != null) { - final result = await hasAnyBlockers(); - if (result) { - return; - } - await _setTotal(index); - if (index == 1) { - await _updateWidget( - text: "First memory fetched. updating widget", - ); - } - index++; + int renderedCount = 0; + int attemptsCount = 0; - if (index >= 50) { - _logger.warning(">>> Max memory limit reached"); - break; - } + await updateMemoriesStatus(WidgetStatus.notSynced); + + final memoriesWithFilesLength = memoriesWithFiles.length; + final memoriesWithFilesEntries = memoriesWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + memoriesWithFilesEntries[random.nextInt(memoriesWithFilesLength)]; + + if (randomEntry.value.isEmpty) continue; + + final randomMemoryFile = randomEntry.value.elementAt( + random.nextInt(randomEntry.value.length), + ); + final memoryTitle = randomEntry.key; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomMemoryFile, + "memory_widget_$renderedCount", + memoryTitle, + null, + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + return; } + + await _setTotalMemories(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First memory fetched, updating widget", + ); + await updateMemoriesStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; } - if (index >= 50) { - break; - } + attemptsCount++; } - if (index == 0) { + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + if (renderedCount == 0) { return; } - await _updateWidget( - text: ">>> Switching to next memory set, total: $index", - ); - } + if (isWidgetPresent) { + await updateMemoriesStatus(WidgetStatus.syncedAll); + } - Future onLaunchFromWidget(int generatedId, BuildContext context) async { - _hasSyncedMemory = true; - await _memorySync(); - - await memoriesCacheService.goToMemoryFromGeneratedFileID( - context, - generatedId, + await _refreshWidget( + message: "Switched to next memory set, total: $renderedCount", ); } } diff --git a/mobile/lib/services/notification_service.dart b/mobile/lib/services/notification_service.dart index 86cabfe014..c315d3e410 100644 --- a/mobile/lib/services/notification_service.dart +++ b/mobile/lib/services/notification_service.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import "package:flutter_timezone/flutter_timezone.dart"; import "package:logging/logging.dart"; import "package:photos/services/sync/remote_sync_service.dart"; import "package:shared_preferences/shared_preferences.dart"; +import 'package:timezone/data/latest_10y.dart' as tzdb; +import "package:timezone/timezone.dart" as tz; class NotificationService { static final NotificationService instance = @@ -24,11 +27,14 @@ class NotificationService { _preferences = preferences; } + bool timezoneInitialized = false; + Future initialize( void Function( NotificationResponse notificationResponse, ) onNotificationTapped, ) async { + await initTimezones(); const androidSettings = AndroidInitializationSettings('notification_icon'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: false, @@ -59,6 +65,14 @@ class NotificationService { } } + Future initTimezones() async { + if (timezoneInitialized) return; + tzdb.initializeTimeZones(); + final String currentTimeZone = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(currentTimeZone)); + timezoneInitialized = true; + } + Future requestPermissions() async { bool? result; if (Platform.isIOS) { @@ -127,4 +141,141 @@ class NotificationService { payload: payload, ); } + + Future scheduleNotification( + String title, { + String? message, + required int id, + String channelID = "io.ente.photos", + String channelName = "ente", + String payload = "ente://home", + required DateTime dateTime, + Duration? timeoutDurationAndroid, + }) async { + try { + _logger.info( + "Scheduling notification with: $title, $message, $channelID, $channelName, $payload", + ); + await initTimezones(); + if (!hasGrantedPermissions()) { + _logger.warning("Notification permissions not granted"); + await requestPermissions(); + if (!hasGrantedPermissions()) { + _logger.severe("Failed to get notification permissions"); + return; + } + } else { + _logger.info("Notification permissions already granted"); + } + final androidSpecs = AndroidNotificationDetails( + channelID, + channelName, + channelDescription: 'ente alerts', + importance: Importance.max, + priority: Priority.high, + category: AndroidNotificationCategory.reminder, + showWhen: false, + ); + final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID); + final platformChannelSpecs = + NotificationDetails(android: androidSpecs, iOS: iosSpecs); + final scheduledDate = tz.TZDateTime.local( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); + // final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(delay); + await _notificationsPlugin.zonedSchedule( + id, + title, + message, + scheduledDate, + platformChannelSpecs, + androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, + payload: payload, + ); + _logger.info( + "Scheduled notification with: $title, $message, $channelID, $channelName, $payload for $dateTime", + ); + } catch (e, s) { + // For now we're swallowing any exceptions here because we don't want the memories logic to get disturbed + _logger.severe( + "Something went wrong while scheduling notification", + e, + s, + ); + } + final androidSpecs = AndroidNotificationDetails( + channelID, + channelName, + channelDescription: 'ente alerts', + importance: Importance.max, + priority: Priority.high, + category: AndroidNotificationCategory.reminder, + showWhen: false, + timeoutAfter: timeoutDurationAndroid?.inMilliseconds, + ); + final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID); + final platformChannelSpecs = + NotificationDetails(android: androidSpecs, iOS: iosSpecs); + final scheduledDate = tz.TZDateTime.local( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); + await _notificationsPlugin.zonedSchedule( + id, + title, + message, + scheduledDate, + platformChannelSpecs, + androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, + payload: payload, + ); + _logger.info( + "Scheduled notification with: $title, $message, $channelID, $channelName, $payload", + ); + } + + Future clearAllScheduledNotifications({ + String? containingPayload, + }) async { + try { + _logger.info("Clearing all scheduled notifications"); + final pending = await _notificationsPlugin.pendingNotificationRequests(); + if (pending.isEmpty) { + _logger.info("No pending notifications to clear"); + return; + } + for (final request in pending) { + if (containingPayload != null && + !request.payload.toString().contains(containingPayload)) { + _logger.info( + "Skip clearing of notification with id: ${request.id} and payload: ${request.payload}", + ); + continue; + } + _logger.info( + "Clearing notification with id: ${request.id} and payload: ${request.payload}", + ); + await _notificationsPlugin.cancel(request.id); + _logger.info( + "Cleared notification with id: ${request.id} and payload: ${request.payload}", + ); + } + } catch (e, s) { + _logger.severe("Something is wrong with scheduled notifications", e, s); + } + } + + Future pendingNotifications() async { + final pending = await _notificationsPlugin.pendingNotificationRequests(); + return pending.length; + } } diff --git a/mobile/lib/services/people_home_widget_service.dart b/mobile/lib/services/people_home_widget_service.dart new file mode 100644 index 0000000000..d9f618f6fc --- /dev/null +++ b/mobile/lib/services/people_home_widget_service.dart @@ -0,0 +1,455 @@ +import "dart:math"; + +import "package:collection/collection.dart"; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/file/file.dart'; +import "package:photos/models/search/generic_search_result.dart"; +import "package:photos/models/search/search_constants.dart"; +import "package:photos/models/search/search_types.dart"; +import 'package:photos/service_locator.dart'; +import 'package:photos/services/home_widget_service.dart'; +import 'package:photos/services/machine_learning/face_ml/person/person_service.dart'; +import 'package:photos/services/search_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import "package:photos/ui/viewer/file/detail_page.dart"; +import "package:photos/ui/viewer/people/people_page.dart"; +import 'package:photos/utils/navigation_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:synchronized/synchronized.dart'; + +class PeopleHomeWidgetService { + // Constants + static const String SELECTED_PEOPLE_KEY = "selectedPeopleHW"; + static const String ANDROID_CLASS_NAME = "EntePeopleWidgetProvider"; + static const String IOS_CLASS_NAME = "EntePeopleWidget"; + static const String PEOPLE_STATUS_KEY = "peopleStatusKey.widget"; + static const String PEOPLE_CHANGED_KEY = "peopleChanged.widget"; + static const String TOTAL_PEOPLE_KEY = "totalPeople"; + static const int MAX_PEOPLE_LIMIT = 50; + + // Singleton pattern + static final PeopleHomeWidgetService instance = + PeopleHomeWidgetService._privateConstructor(); + PeopleHomeWidgetService._privateConstructor(); + + // Properties + final Logger _logger = Logger((PeopleHomeWidgetService).toString()); + late final SharedPreferences _prefs; + final _peopleForceRefreshLock = Lock(); + bool _hasSyncedPeople = false; + + // Initialization + void init(SharedPreferences prefs) { + _prefs = prefs; + } + + // Public methods + List? getSelectedPeople() { + return _prefs.getStringList(SELECTED_PEOPLE_KEY); + } + + Future setSelectedPeople(List selectedPeople) async { + final previousSelection = getSelectedPeople(); + await _prefs.setStringList(SELECTED_PEOPLE_KEY, selectedPeople); + + if (previousSelection != null) { + final oldSet = previousSelection.toSet(); + final newSet = selectedPeople.toSet(); + + if (oldSet.containsAll(newSet) && newSet.containsAll(oldSet)) { + _logger.info("People selection unchanged, no update needed"); + return; + } + } + + _logger.info("People selection changed, updating widget"); + await updatePeopleChanged(true); + } + + Future countHomeWidgets() async { + return await HomeWidgetService.instance.countHomeWidgets( + ANDROID_CLASS_NAME, + IOS_CLASS_NAME, + ); + } + + Future initHomeWidget(bool? forceFetchNewPeople) async { + if (await _hasAnyBlockers()) { + await clearWidget(); + return; + } + + await _peopleForceRefreshLock.synchronized(() async { + if (await _hasAnyBlockers()) { + return; + } + + final isPeopleEmpty = await _isWidgetEmpty(); + forceFetchNewPeople ??= await _shouldForceFetchPeople(isPeopleEmpty); + + _logger.warning( + "Initializing people widget: forceFetch: $forceFetchNewPeople, isPeopleEmpty: $isPeopleEmpty", + ); + + if (forceFetchNewPeople!) { + await _forcePeopleUpdate(); + } else if (!isPeopleEmpty) { + await _syncExistingPeople(); + } + }); + } + + Future clearWidget() async { + if (await _isWidgetEmpty()) { + _logger.info("Widget already empty, nothing to clear"); + return; + } + + _logger.info("Clearing PeopleHomeWidget"); + await _setTotalPeople(null); + _hasSyncedPeople = false; + await updatePeopleStatus(WidgetStatus.syncedEmpty); + await _refreshWidget(message: "PeopleHomeWidget cleared & updated"); + } + + WidgetStatus getPeopleStatus() { + return WidgetStatus.values.firstWhereOrNull( + (v) => v.index == (_prefs.getInt(PEOPLE_STATUS_KEY) ?? 0), + ) ?? + WidgetStatus.notSynced; + } + + Future updatePeopleStatus(WidgetStatus value) async { + await _prefs.setInt(PEOPLE_STATUS_KEY, value.index); + } + + Future updatePeopleChanged(bool value) async { + _logger.info("Updating people changed flag to $value"); + await _prefs.setBool(PEOPLE_CHANGED_KEY, value); + } + + Future checkPendingPeopleSync({bool addDelay = true}) async { + if (addDelay) { + await Future.delayed(const Duration(seconds: 5)); + } + + final isPeopleEmpty = await _isWidgetEmpty(); + final needsForceFetch = await _shouldForceFetchPeople(isPeopleEmpty); + + if (_hasSyncedPeople && !needsForceFetch) { + _logger.info("People already synced, no action needed"); + return; + } + + await HomeWidgetService.instance.initHomeWidget(); + } + + Future peopleChanged() async { + await updatePeopleChanged(true); + + final cachedMemories = await _getPeople(); + final currentTotal = cachedMemories.length; + final existingTotal = await _getTotalPeople() ?? 0; + + if (existingTotal == currentTotal && existingTotal == 0) { + await updatePeopleChanged(false); + _logger.info("People empty, no update needed"); + return; + } + + _logger.info("People changed, updating widget"); + await initHomeWidget(true); + } + + Future onLaunchFromWidget( + int fileId, + String personId, + BuildContext context, + ) async { + _hasSyncedPeople = true; + await _syncExistingPeople(); + + final file = await FilesDB.instance.getFile(fileId); + if (file == null) { + _logger.warning("Cannot launch widget: file with ID $fileId not found"); + return; + } + + final person = await PersonService.instance.getPerson(personId); + if (person == null) { + _logger + .warning("Cannot launch widget: person with ID $personId not found"); + return; + } + + routeToPage( + context, + PeoplePage( + person: person, + searchResult: null, + ), + forceCustomPageRoute: true, + ).ignore(); + + final clusterFiles = + await SearchService.instance.getClusterFilesForPersonID( + personId, + ); + final files = clusterFiles.entries.expand((e) => e.value).toList(); + + await routeToPage( + context, + DetailPage( + DetailPageConfiguration( + files, + files.indexOf(file), + "peoplewidget", + ), + ), + forceCustomPageRoute: true, + ); + } + + // Private methods + Future _forcePeopleUpdate() async { + await _loadAndRenderPeople(); + await updatePeopleChanged(false); + } + + Future _hasAnyBlockers() async { + // Check if first import is completed + final hasCompletedFirstImport = + LocalSyncService.instance.hasCompletedFirstImport(); + if (!hasCompletedFirstImport) { + _logger.warning("First import not completed"); + return true; + } + + // Check ML consent + if (!flagService.hasGrantedMLConsent) { + _logger.warning("ML consent not granted"); + return true; + } + + // Check if selected people exist + final peopleIds = await _getEffectiveSelectedPeopleIds(); + try { + for (final id in peopleIds) { + final person = await PersonService.instance.getPerson(id); + if (person == null) { + _logger.warning("Person not found for id: $id"); + return true; + } + } + } catch (e) { + _logger.warning("Error looking up people: $e"); + return true; + } + + return false; + } + + Future _syncExistingPeople() async { + final homeWidgetCount = await countHomeWidgets(); + if (homeWidgetCount == 0) { + _logger.warning("No active home widgets found"); + return; + } + + await _refreshWidget(message: "Refreshing from existing people set"); + } + + Future _isWidgetEmpty() async { + final totalPeople = await _getTotalPeople(); + return totalPeople == 0 || totalPeople == null; + } + + Future _shouldForceFetchPeople(bool isPeopleEmpty) async { + final peopleChanged = _prefs.getBool(PEOPLE_CHANGED_KEY); + if (peopleChanged ?? true) { + return true; + } + + final peopleStatus = getPeopleStatus(); + switch (peopleStatus) { + case WidgetStatus.notSynced: + return true; + case WidgetStatus.syncedPartially: + return await countHomeWidgets() > 0; + case WidgetStatus.syncedEmpty: + case WidgetStatus.syncedAll: + return false; + } + } + + Future> _getEffectiveSelectedPeopleIds() async { + var peopleIds = getSelectedPeople(); + + if (peopleIds == null || peopleIds.isEmpty) { + // Search Filter with face and pick top two faces + final searchFilter = await SectionType.face.getData(null).then( + (value) => (value as List).where( + (element) => (element.params[kPersonParamID] as String?) != null, + ), + ); + + if (searchFilter.isNotEmpty) { + peopleIds = searchFilter + .take(2) + .map((e) => e.params[kPersonParamID] as String) + .toList(); + } else { + _logger.warning("No selected people found"); + } + } + + return peopleIds ?? []; + } + + Future)>> _getPeople() async { + final peopleIds = await _getEffectiveSelectedPeopleIds(); + final Map)> peopleFiles = {}; + + for (final id in peopleIds) { + final person = await PersonService.instance.getPerson(id); + if (person == null) { + _logger.warning("Person not found for id: $id"); + continue; + } + + final clusterFiles = + await SearchService.instance.getClusterFilesForPersonID(id); + final files = clusterFiles.entries.expand((e) => e.value).toList(); + if (files.isEmpty) { + _logger.warning("No files found for person: ${person.data.name}"); + continue; + } + peopleFiles[id] = (person.data.name, files); + } + + return peopleFiles; + } + + Future _refreshWidget({String? message}) async { + await HomeWidgetService.instance.updateWidget( + androidClass: ANDROID_CLASS_NAME, + iOSClass: IOS_CLASS_NAME, + ); + + if (flagService.internalUser) { + await Fluttertoast.showToast( + msg: "[i][ppl] ${message ?? "PeopleHomeWidget updated"}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + _logger.info("Home Widget updated: ${message ?? "standard update"}"); + } + + Future _getTotalPeople() async { + return HomeWidgetService.instance.getData(TOTAL_PEOPLE_KEY); + } + + Future _setTotalPeople(int? total) async { + await HomeWidgetService.instance.setData(TOTAL_PEOPLE_KEY, total); + } + + Future _loadAndRenderPeople() async { + final peopleWithFiles = await _getPeople(); + + if (peopleWithFiles.isEmpty) { + _logger.warning("No files found for any people, clearing widget"); + await clearWidget(); + return; + } + + final currentTotal = await _getTotalPeople(); + _logger.info("Current total people in widget: $currentTotal"); + + final bool isWidgetPresent = await countHomeWidgets() > 0; + + final limit = isWidgetPresent ? MAX_PEOPLE_LIMIT : 5; + final maxAttempts = limit * 10; + + int renderedCount = 0; + int attemptsCount = 0; + + await updatePeopleStatus(WidgetStatus.notSynced); + + final peopleWithFilesLength = peopleWithFiles.length; + final peopleWithFilesEntries = peopleWithFiles.entries.toList(); + final random = Random(); + + while (renderedCount < limit && attemptsCount < maxAttempts) { + final randomEntry = + peopleWithFilesEntries[random.nextInt(peopleWithFilesLength)]; + + if (randomEntry.value.$2.isEmpty) continue; + + final randomPersonFile = randomEntry.value.$2.elementAt( + random.nextInt(randomEntry.value.$2.length), + ); + final personId = randomEntry.key; + final personName = randomEntry.value.$1; + + final renderResult = await HomeWidgetService.instance + .renderFile( + randomPersonFile, + "people_widget_$renderedCount", + personName, + personId, + ) + .catchError((e, stackTrace) { + _logger.severe("Error rendering widget", e, stackTrace); + return null; + }); + + if (renderResult != null) { + // Check for blockers again before continuing + if (await _hasAnyBlockers()) { + return; + } + + await _setTotalPeople(renderedCount); + + // Show update toast after first item is rendered + if (renderedCount == 1) { + await _refreshWidget( + message: "First person fetched, updating widget", + ); + await updatePeopleStatus(WidgetStatus.syncedPartially); + } + + renderedCount++; + } + + attemptsCount++; + } + + if (attemptsCount >= maxAttempts) { + _logger.warning( + "Hit max attempts $maxAttempts. Only rendered $renderedCount of limit $limit.", + ); + } + + if (renderedCount == 0) { + return; + } + + if (isWidgetPresent) { + await updatePeopleStatus(WidgetStatus.syncedAll); + } + + await _refreshWidget( + message: "Switched to next people set, total: $renderedCount", + ); + } +} diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 1c2e6ed2ab..9ea96d3bc9 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -5,6 +5,8 @@ import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:intl/intl.dart"; import 'package:logging/logging.dart'; +import "package:path_provider/path_provider.dart"; +import "package:photos/core/configuration.dart"; import "package:photos/core/constants.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/data/holidays.dart'; @@ -23,6 +25,7 @@ import 'package:photos/models/file/file_type.dart'; import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/location_tag/location_tag.dart"; +import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/models/memories/smart_memory.dart"; import "package:photos/models/ml/face/person.dart"; @@ -46,12 +49,14 @@ import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; +import "package:photos/services/memories_cache_service.dart"; import "package:photos/states/location_screen_state.dart"; import "package:photos/ui/viewer/location/add_location_sheet.dart"; import "package:photos/ui/viewer/location/location_screen.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; import "package:photos/ui/viewer/people/people_page.dart"; import "package:photos/ui/viewer/search/result/magic_result_screen.dart"; +import "package:photos/utils/cache_util.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/navigation_util.dart"; import 'package:photos/utils/standalone/date_time.dart'; @@ -1272,9 +1277,35 @@ class SearchService { final cache = await memoriesCacheService.debugCacheForTesting(); final memoriesResult = await smartMemoriesService - .calcMemories(calcTime, cache, debugSurfaceAll: true); + .calcSmartMemories(calcTime, cache, debugSurfaceAll: true); locationService.baseLocations = memoriesResult.baseLocations; - memories = memoriesResult.memories; + for (final nowMemory in memoriesResult.memories) { + cache.toShowMemories + .add(ToShowMemory.fromSmartMemory(nowMemory, calcTime)); + } + cache.baseLocations.addAll(memoriesResult.baseLocations); + // memories = memoriesResult.memories; + final tempCachePath = (await getTemporaryDirectory()).path + + "/cache/test/memories_cache_test"; + await writeToJsonFile( + tempCachePath, + cache, + MemoriesCache.encodeToJsonString, + ); + _logger.info( + "Smart memories cache written to $tempCachePath", + ); + final decodedCache = await decodeJsonFile( + tempCachePath, + MemoriesCache.decodeFromJsonString, + ); + _logger.info( + "Smart memories cache decoded from $tempCachePath", + ); + memories = await MemoriesCacheService.fromCacheToMemories(decodedCache!); + _logger.info( + "Smart memories cache converted to memories", + ); } final searchResults = []; for (final memory in memories) { @@ -1413,10 +1444,29 @@ class SearchService { int? limit, ) async { try { + final int ownerID = Configuration.instance.getUserID()!; final searchResults = []; final allFiles = await getAllFilesForSearch(); final peopleToSharedFiles = >{}; + final peopleToSharedAlbums = >{}; final existingEmails = {}; + final familyEmails = UserService.instance.getEmailIDsOfFamilyMember(); + final List collections = _collectionService + .getCollectionsForUI(includedShared: true, includeCollab: true); + + for (Collection collection in collections) { + if (collection.isHidden() || + collection.isArchived() || + collection.isOwner(ownerID)) { + continue; + } + + if (peopleToSharedAlbums.containsKey(collection.owner.email)) { + peopleToSharedAlbums[collection.owner.email]!.add(collection); + } else { + peopleToSharedAlbums[collection.owner.email] = [collection]; + } + } int peopleCount = 0; for (EnteFile file in allFiles) { @@ -1460,31 +1510,55 @@ class SearchService { } } - peopleToSharedFiles.forEach((key, value) { - final name = key.displayName != null && key.displayName!.isNotEmpty - ? key.displayName! - : key.email; + final sortedEntries = peopleToSharedFiles.entries.toList(); + sortedEntries.sort((a, b) { + final isAFamily = familyEmails.contains(a.key.email); + final isBFamily = familyEmails.contains(b.key.email); + if (isAFamily != isBFamily) { + return isAFamily ? -1 : 1; + } + + final countComparison = b.value.length.compareTo(a.value.length); + if (countComparison != 0) { + return countComparison; + } + + final aName = + a.key.displayName?.toLowerCase() ?? a.key.email.toLowerCase(); + final bName = + b.key.displayName?.toLowerCase() ?? b.key.email.toLowerCase(); + return aName.compareTo(bName); + }); + + for (var entry in sortedEntries) { + final user = entry.key; + final files = entry.value; + final name = user.displayName != null && user.displayName!.isNotEmpty + ? user.displayName! + : user.email; + final collections = peopleToSharedAlbums[user.email] ?? []; searchResults.add( GenericSearchResult( ResultType.shared, name, - value, + files, hierarchicalSearchFilter: ContactsFilter( - user: key, + user: user, occurrence: kMostRelevantFilter, - matchedUploadedIDs: filesToUploadedFileIDs(value), + matchedUploadedIDs: filesToUploadedFileIDs(files), ), params: { - kPersonParamID: key.linkedPersonID, - kContactEmail: key.email, + kPersonParamID: user.linkedPersonID, + kContactEmail: user.email, + kContactCollections: collections, }, ), ); - }); + } return searchResults; } catch (e) { - _logger.severe("Error in getAllLocationTags", e); + _logger.severe("Error in getAllContactSearchResults", e); return []; } } diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 5b66526b36..f523ecc48b 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -21,6 +21,7 @@ import "package:photos/models/memories/clip_memory.dart"; import "package:photos/models/memories/filler_memory.dart"; import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/on_this_day_memory.dart"; import "package:photos/models/memories/people_memory.dart"; import "package:photos/models/memories/smart_memory.dart"; import "package:photos/models/memories/smart_memory_constants.dart"; @@ -30,6 +31,7 @@ import "package:photos/models/ml/face/face_with_embedding.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/models/ml/vector.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/collections_service.dart"; import "package:photos/services/language_service.dart"; import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; @@ -61,7 +63,7 @@ class SmartMemoriesService { SmartMemoriesService(); // One general method to get all memories, which calls on internal methods for each separate memory type - Future calcMemories( + Future calcSmartMemories( DateTime now, MemoriesCache oldCache, { bool debugSurfaceAll = false, @@ -75,6 +77,11 @@ class SmartMemoriesService { final (allFiles, allFileIdsToFile) = await _getFilesAndMapForMemories(); _logger.finest("All files length: ${allFiles.length} $t"); + final collectionIDsToExclude = await getCollectionIDsToExclude(); + _logger.finest( + 'collectionIDsToExclude length: ${collectionIDsToExclude.length} $t', + ); + final seenTimes = await _memoriesDB.getSeenTimes(); _logger.finest('seenTimes has ${seenTimes.length} entries $t'); @@ -127,6 +134,7 @@ class SmartMemoriesService { param: { "allFiles": allFiles, "allFileIdsToFile": allFileIdsToFile, + "collectionIDsToExclude": collectionIDsToExclude, "now": now, "oldCache": oldCache, "debugSurfaceAll": debugSurfaceAll, @@ -194,6 +202,7 @@ class SmartMemoriesService { // Arguments: direct data final Set allFiles = args["allFiles"]; final Map allFileIdsToFile = args["allFileIdsToFile"]; + final Set collectionIDsToExclude = args["collectionIDsToExclude"]; final DateTime now = args["now"]; final MemoriesCache oldCache = args["oldCache"]; final bool debugSurfaceAll = args["debugSurfaceAll"] ?? false; @@ -230,6 +239,17 @@ class SmartMemoriesService { final List memories = []; + // On this day memories + final onThisDayMemories = await _getOnThisDayResults( + allFiles, + now, + seenTimes: seenTimes, + collectionIDsToExclude: collectionIDsToExclude, + ); + _deductUsedMemories(allFiles, onThisDayMemories); + memories.addAll(onThisDayMemories); + dev.log("All files length after on this day: ${allFiles.length} $t"); + // People memories final peopleMemories = await _getPeopleResults( allFiles, @@ -252,6 +272,7 @@ class SmartMemoriesService { // Trip memories final (tripMemories, bases) = await _getTripsResults( allFiles, + allFileIdsToFile, now, oldCache.tripsShownLogs, surfaceAll: debugSurfaceAll, @@ -308,22 +329,41 @@ class SmartMemoriesService { } } - Future> calcFillerResults() async { + Future> calcSimpleMemories() async { final now = DateTime.now(); final (allFiles, _) = await _getFilesAndMapForMemories(); final seenTimes = await _memoriesDB.getSeenTimes(); + final collectionIDsToExclude = await getCollectionIDsToExclude(); + + final List memories = []; + + // On this day memories + final onThisDayMemories = await _getOnThisDayResults( + allFiles, + now, + seenTimes: seenTimes, + collectionIDsToExclude: collectionIDsToExclude, + ); + if (onThisDayMemories.isNotEmpty && + onThisDayMemories.first.shouldShowNow()) { + memories.add(onThisDayMemories.first); + _deductUsedMemories(allFiles, [onThisDayMemories.first]); + } + + // Filler memories final fillerMemories = await _getFillerResults(allFiles, now, seenTimes: seenTimes); + memories.addAll(fillerMemories); final local = await getLocale(); final languageCode = local?.languageCode ?? "en"; final s = await LanguageService.s; _logger.finest('get locale and S'); - for (final memory in fillerMemories) { + for (final memory in memories) { memory.title = memory.createTitle(s, languageCode); } - return fillerMemories; + return memories; } static void _deductUsedMemories( @@ -378,7 +418,7 @@ class SmartMemoriesService { } } final List orderedImportantPersonsID = persons - .where((person) => !person.data.isHidden) + .where((person) => !person.data.isHidden && !person.data.isIgnored) .map((p) => p.remoteID) .toList(); orderedImportantPersonsID.shuffle(Random()); @@ -647,9 +687,19 @@ class SmartMemoriesService { if (youAndThemMem != null) { memoryResults.add( youAndThemMem.copyWith( + isBirthday: false, + newAge: newAge, firstDateToShow: thisBirthday .subtract(const Duration(days: 5)) .microsecondsSinceEpoch, + lastDateToShow: thisBirthday.microsecondsSinceEpoch, + ), + ); + memoryResults.add( + youAndThemMem.copyWith( + isBirthday: true, + newAge: newAge, + firstDateToShow: thisBirthday.microsecondsSinceEpoch, lastDateToShow: thisBirthday.add(kDayItself).microsecondsSinceEpoch, ), @@ -689,7 +739,7 @@ class SmartMemoriesService { 'Something is going wrong, ${potentialMemory.peopleMemoryType} has multiple memories for same person', ); } else { - final randIdx = Random().nextInt(potentialMemory.memories.length); + final randIdx = Random().nextInt(memoriesForCategory.length); potentialMemory = memoriesForCategory[randIdx]; } } @@ -802,6 +852,7 @@ class SmartMemoriesService { static Future<(List, List)> _getTripsResults( Iterable allFiles, + Map allFileIdsToFile, DateTime currentTime, List shownTrips, { bool surfaceAll = false, @@ -907,7 +958,13 @@ class SmartMemoriesService { const Duration(days: 90), ), ); - baseLocations.add(BaseLocation(files, location, isCurrent)); + baseLocations.add( + BaseLocation( + files.map((file) => file.uploadedFileID!).toList(), + location, + isCurrent, + ), + ); } // Identify trip locations @@ -1059,8 +1116,11 @@ class SmartMemoriesService { for (final baseLocation in baseLocations) { String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; + final files = baseLocation.fileIDs + .map((fileID) => allFileIdsToFile[fileID]!) + .toList(); final String? locationName = _tryFindLocationName( - Memory.fromFiles(baseLocation.files, seenTimes), + Memory.fromFiles(files, seenTimes), cities, base: true, ); @@ -1070,7 +1130,7 @@ class SmartMemoriesService { } memoryResults.add( TripMemory( - Memory.fromFiles(baseLocation.files, seenTimes), + Memory.fromFiles(files, seenTimes), nowInMicroseconds, windowEnd, baseLocation.location, @@ -1542,6 +1602,165 @@ class SmartMemoriesService { return memoryResults; } + Future> getCollectionIDsToExclude() async { + final collections = CollectionsService.instance.getCollectionsForUI(); + + // Names of collections to exclude + const excludedNames = { + 'screenshot', + 'whatsapp', + 'telegram', + 'download', + 'facebook', + 'instagram', + 'messenger', + 'twitter', + 'reddit', + 'discord', + 'signal', + 'viber', + 'wechat', + 'line', + 'meme', + 'internet', + 'saved images', + 'document', + }; + + final excludedCollectionIDs = {}; + collectionLoop: + for (final collection in collections) { + final collectionName = collection.displayName.toLowerCase(); + for (final excludedName in excludedNames) { + if (collectionName.contains(excludedName)) { + excludedCollectionIDs.add(collection.id); + continue collectionLoop; + } + } + } + + return excludedCollectionIDs; + } + + static Future> _getOnThisDayResults( + Iterable allFiles, + DateTime currentTime, { + required Map seenTimes, + required Set collectionIDsToExclude, + }) async { + final List memoryResults = []; + if (allFiles.isEmpty) return []; + + final daysToCompute = kMemoriesUpdateFrequency.inDays; + final currentYear = currentTime.year; + final currentMonth = currentTime.month; + final currentDay = currentTime.day; + final startPoint = DateTime(currentYear, currentMonth, currentDay); + final cutOffTime = startPoint + .subtract(const Duration(days: 363) - kMemoriesUpdateFrequency); + final diffThreshold = Duration(days: daysToCompute); + + final Map> daysToMemories = {}; + final Map> daysToYears = {}; + + final timeTillYearEnd = DateTime(currentYear + 1).difference(startPoint); + final bool almostYearEnd = timeTillYearEnd < diffThreshold; + + // Find all the relevant memories + for (final file in allFiles) { + if (collectionIDsToExclude.contains(file.collectionID)) continue; + if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) { + continue; + } + final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final fileTimeInYear = fileDate.copyWith(year: currentYear); + final diff = fileTimeInYear.difference(startPoint); + if (!diff.isNegative && diff < diffThreshold) { + daysToMemories + .putIfAbsent(diff.inDays, () => []) + .add(Memory.fromFile(file, seenTimes)); + daysToYears.putIfAbsent(diff.inDays, () => []).add(fileDate.year); + } else if (almostYearEnd) { + final altDiff = fileDate.copyWith(year: currentYear + 1).difference( + currentTime, + ); + if (!altDiff.isNegative && altDiff < diffThreshold) { + daysToMemories + .putIfAbsent(altDiff.inDays, () => []) + .add(Memory.fromFile(file, seenTimes)); + daysToYears.putIfAbsent(altDiff.inDays, () => []).add(fileDate.year); + } + } + } + + // Per day, filter the memories to find the best ones + for (var day = 0; day < daysToCompute; day++) { + final memories = daysToMemories[day]; + if (memories == null) continue; + if (memories.length < 5) continue; + final years = daysToYears[day]!; + if (years.toSet().length < 2) continue; + + final filteredMemories = []; + if (memories.length > 20) { + // Group memories by year + final Map> memoriesByYear = {}; + for (final memory in memories) { + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(memory.file.creationTime!); + final year = creationTime.year; + memoriesByYear.putIfAbsent(year, () => []).add(memory); + } + for (final year in memoriesByYear.keys) { + memoriesByYear[year]!.shuffle(Random()); + } + + // Get all years, randonly select 20 years if there are more than 20 + List years = memoriesByYear.keys.toList()..sort(); + if (years.length > 20) { + years.shuffle(Random()); + years = years.take(20).toList()..sort(); + } + + // First round to take one memory from each year + for (final year in years) { + if (filteredMemories.length >= 20) break; + final yearMemories = memoriesByYear[year]!; + if (yearMemories.isNotEmpty) { + filteredMemories.add(yearMemories.removeAt(0)); + } + } + + // Second round to fill up to 20 memories + while (filteredMemories.length < 20) { + bool addedAny = false; + for (final year in years) { + if (filteredMemories.length >= 20) break; + final yearMemories = memoriesByYear[year]!; + if (yearMemories.isNotEmpty) { + filteredMemories.add(yearMemories.removeAt(0)); + addedAny = true; + } + } + if (!addedAny) break; + } + } else { + filteredMemories.addAll(memories); + } + + filteredMemories.sort( + (a, b) => a.file.creationTime!.compareTo(b.file.creationTime!), + ); + final onThisDayMemory = OnThisDayMemory( + filteredMemories, + startPoint.add(Duration(days: day)).microsecondsSinceEpoch, + startPoint.add(Duration(days: day + 1)).microsecondsSinceEpoch, + ); + memoryResults.add(onThisDayMemory); + } + return memoryResults; + } + static Future getDateFormattedLocale({ required int creationTime, }) async { diff --git a/mobile/lib/services/sync/local_sync_service.dart b/mobile/lib/services/sync/local_sync_service.dart index aa630c2d73..0b3a14ec47 100644 --- a/mobile/lib/services/sync/local_sync_service.dart +++ b/mobile/lib/services/sync/local_sync_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; +import "package:photos/core/cache/lru_map.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/core/errors.dart"; import 'package:photos/core/event_bus.dart'; @@ -29,6 +30,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:synchronized/synchronized.dart'; import 'package:tuple/tuple.dart'; +// This map is used to track if a iOS origin file is being fetched for uploading +// or ML processing. In such cases, we want to ignore these files if they come in response +// from the local sync service. When a file is download +final LRUMap trackOriginFetchForUploadOrML = LRUMap(200); + class LocalSyncService { final _logger = Logger("LocalSyncService"); final _db = FilesDB.instance; @@ -297,15 +303,31 @@ class LocalSyncService { conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore, ); _logger.info('Inserted ${files.length} out of ${allFiles.length} files'); - if (allFiles.isNotEmpty) { - Bus.instance.fire( - LocalPhotosUpdatedEvent(allFiles, source: "loadedPhoto"), - ); - } + _checkAndFireLocalAssetUpdateEvent(allFiles, files.isNotEmpty); } await _prefs.setInt(kDbUpdationTimeKey, toTime); } + void _checkAndFireLocalAssetUpdateEvent( + List allFiles, + bool discoveredNewFiles, + ) { + if (allFiles.isEmpty) return; + if (!discoveredNewFiles) { + allFiles.removeWhere( + (file) => + trackOriginFetchForUploadOrML.get(file.localID ?? '') ?? false, + ); + if (allFiles.isEmpty) { + _logger.info("skipping firing LocalPhotosUpdatedEvent as no new files"); + return; + } + } + Bus.instance.fire( + LocalPhotosUpdatedEvent(allFiles, source: "loadedPhoto"), + ); + } + Future _trackUpdatedFiles( List files, Set existingLocalFileIDs, @@ -318,11 +340,20 @@ class LocalSyncService { ) .map((e) => e.localID!) .toList(); + if (updatedLocalIDs.isNotEmpty) { - await FileUpdationDB.instance.insertMultiple( - updatedLocalIDs, - FileUpdationDB.modificationTimeUpdated, + final int updateCount = updatedLocalIDs.length; + updatedLocalIDs + .removeWhere((x) => trackOriginFetchForUploadOrML.get(x) ?? false); + _logger.info( + "track ${updatedLocalIDs.length}/ $updateCount files due to modification change", ); + if (updatedLocalIDs.isEmpty) { + await FileUpdationDB.instance.insertMultiple( + updatedLocalIDs, + FileUpdationDB.modificationTimeUpdated, + ); + } } } diff --git a/mobile/lib/services/sync/remote_sync_service.dart b/mobile/lib/services/sync/remote_sync_service.dart index bedbc773d4..53b5f01610 100644 --- a/mobile/lib/services/sync/remote_sync_service.dart +++ b/mobile/lib/services/sync/remote_sync_service.dart @@ -25,13 +25,13 @@ import 'package:photos/models/upload_strategy.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/filedata/filedata_service.dart"; import 'package:photos/services/ignored_files_service.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/local_file_update_service.dart'; import "package:photos/services/notification_service.dart"; -import "package:photos/services/preview_video_store.dart"; import 'package:photos/services/sync/diff_fetcher.dart'; import 'package:photos/services/sync/sync_service.dart'; +import "package:photos/services/video_preview_service.dart"; import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/file_util.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -127,8 +127,11 @@ class RemoteSyncService { await syncDeviceCollectionFilesForUpload(); } - FileDataService.instance.syncFDStatus().then((_) { - PreviewVideoStore.instance.queueFiles(); + fileDataService.syncFDStatus().then((_) { + if (!flagService.hasGrantedMLConsent) { + VideoPreviewService.instance + .queueFiles(); // if ML is enabled the MLService will queue when ML is done + } }).ignore(); final filesToBeUploaded = await _getFilesToBeUploaded(); final hasUploadedFiles = await _uploadFiles(filesToBeUploaded); @@ -572,7 +575,7 @@ class RemoteSyncService { _logger.info("Skipped $skippedVideos videos and $ignoredForUpload " "ignored files for upload"); } - _sortByTimeAndType(filesToBeUploaded); + _sortByTime(filesToBeUploaded); _logger.info("${filesToBeUploaded.length} new files to be uploaded."); return filesToBeUploaded; } @@ -933,17 +936,11 @@ class RemoteSyncService { return Platform.isIOS && !AppLifecycleService.instance.isForeground; } - // _sortByTimeAndType moves videos to end and sort by creation time (desc). + // _sortByTime sort by creation time (desc). // This is done to upload most recent photo first. - void _sortByTimeAndType(List file) { + void _sortByTime(List file) { file.sort((first, second) { - if (first.fileType == second.fileType) { - return second.creationTime!.compareTo(first.creationTime!); - } else if (first.fileType == FileType.video) { - return 1; - } else { - return -1; - } + return second.creationTime!.compareTo(first.creationTime!); }); // move updated files towards the end file.sort((first, second) { @@ -998,10 +995,11 @@ class RemoteSyncService { 'creating notification for ${collection?.displayName} ' 'shared: $sharedFilesIDs, collected: $collectedFilesIDs files', ); + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( collection!.displayName, - totalCount.toString() + " new 📸", + totalCount.toString() + s.newPhotosEmoji, channelID: "collection:" + collectionID.toString(), channelName: collection.displayName, payload: "ente://collection/?collectionID=" + collectionID.toString(), diff --git a/mobile/lib/services/sync/sync_service.dart b/mobile/lib/services/sync/sync_service.dart index 16bb94fa05..ac1ee5bb45 100644 --- a/mobile/lib/services/sync/sync_service.dart +++ b/mobile/lib/services/sync/sync_service.dart @@ -13,6 +13,7 @@ import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/events/sync_status_update_event.dart'; import 'package:photos/events/trigger_logout_event.dart'; import 'package:photos/models/file/file_type.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/notification_service.dart'; import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/services/sync/remote_sync_service.dart'; @@ -207,10 +208,11 @@ class SyncService { final now = DateTime.now().microsecondsSinceEpoch; if ((now - lastNotificationShownTime) > microSecondsInDay) { await _prefs.setInt(kLastStorageLimitExceededNotificationPushTime, now); + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( - "Storage limit exceeded", - "Sorry, we had to pause your backups", + s.storageLimitExceeded, + s.sorryWeHadToPauseYourBackups, ); } } diff --git a/mobile/lib/services/sync/trash_sync_service.dart b/mobile/lib/services/sync/trash_sync_service.dart index 53c94f16c7..b3cf1db6a9 100644 --- a/mobile/lib/services/sync/trash_sync_service.dart +++ b/mobile/lib/services/sync/trash_sync_service.dart @@ -7,6 +7,7 @@ import "package:ente_crypto/ente_crypto.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; +import "package:photos/db/files_db.dart"; import 'package:photos/db/trash_db.dart'; import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/force_reload_trash_page_event.dart'; @@ -17,6 +18,7 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/trash_file.dart'; import 'package:photos/models/ignored_file.dart'; import "package:photos/models/metadata/file_magic.dart"; +import "package:photos/services/collections_service.dart"; import 'package:photos/services/ignored_files_service.dart'; import "package:photos/utils/file_key.dart"; import 'package:shared_preferences/shared_preferences.dart'; @@ -108,10 +110,34 @@ class TrashSyncService { Future trashFilesOnServer(List trashRequestItems) async { final includedFileIDs = {}; final uniqueItems = []; + final ownedCollectionIDs = + CollectionsService.instance.getAllOwnedCollectionIDs(); for (final item in trashRequestItems) { if (!includedFileIDs.contains(item.fileID)) { - uniqueItems.add(item); - includedFileIDs.add(item.fileID); + // Check if the collectionID in the request is owned by the user + if (ownedCollectionIDs.contains(item.collectionID)) { + uniqueItems.add(item); + includedFileIDs.add(item.fileID); + } else { + // If not owned, use a different owned collectionID + final fileCollectionIDs = + await FilesDB.instance.getAllCollectionIDsOfFile(item.fileID); + bool foundAnotherOwnedCollection = false; + for (final collectionID in fileCollectionIDs) { + if (ownedCollectionIDs.contains(collectionID)) { + final newItem = TrashRequest(item.fileID, collectionID); + uniqueItems.add(newItem); + includedFileIDs.add(item.fileID); + foundAnotherOwnedCollection = true; + break; + } + } + if (!foundAnotherOwnedCollection) { + _logger.severe( + "File ${item.fileID} is not owned by the user and has no other owned collection", + ); + } + } } } final requestData = {}; diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 4b73cbad6c..10b5589ebd 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/network/network.dart'; +import "package:photos/services/language_service.dart"; import 'package:photos/services/notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -13,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class UpdateService { static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 27; + static const currentChangeLogVersion = 28; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); @@ -90,10 +91,11 @@ class UpdateService { Future showUpdateNotification() async { if (await shouldShowUpdateNotification()) { + final s = await LanguageService.s; // ignore: unawaited_futures NotificationService.instance.showNotification( - "Update available", - "Click to install our best version yet", + s.updateAvailable, + s.clickToInstallOurBestVersionYet, ); await resetUpdateAvailableShownTime(); } else { diff --git a/mobile/lib/services/preview_video_store.dart b/mobile/lib/services/video_preview_service.dart similarity index 71% rename from mobile/lib/services/preview_video_store.dart rename to mobile/lib/services/video_preview_service.dart index f723fb4a81..bfcce8c381 100644 --- a/mobile/lib/services/preview_video_store.dart +++ b/mobile/lib/services/video_preview_service.dart @@ -9,7 +9,6 @@ import "package:encrypt/encrypt.dart" as enc; import "package:ffmpeg_kit_flutter/ffmpeg_session.dart"; import "package:ffmpeg_kit_flutter/return_code.dart"; import "package:flutter/foundation.dart"; -// import "package:flutter/wid.dart"; import "package:flutter/widgets.dart"; import "package:flutter_cache_manager/flutter_cache_manager.dart"; import "package:logging/logging.dart"; @@ -20,7 +19,6 @@ import "package:photos/core/event_bus.dart"; import "package:photos/core/network/network.dart"; import 'package:photos/db/files_db.dart'; import "package:photos/db/upload_locks_db.dart"; -import "package:photos/events/preview_updated_event.dart"; import "package:photos/events/video_streaming_changed.dart"; import "package:photos/models/base/id.dart"; import "package:photos/models/ffmpeg/ffprobe_props.dart"; @@ -29,7 +27,9 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/models/preview/playlist_data.dart"; import "package:photos/models/preview/preview_item.dart"; import "package:photos/models/preview/preview_item_status.dart"; -import "package:photos/services/filedata/filedata_service.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/services/filedata/model/file_data.dart"; import "package:photos/services/isolated_ffmpeg_service.dart"; import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/exif_util.dart"; @@ -41,26 +41,28 @@ import "package:shared_preferences/shared_preferences.dart"; const _maxRetryCount = 3; -class PreviewVideoStore { +class VideoPreviewService { + final _logger = Logger("VideoPreviewService"); final LinkedHashMap _items = LinkedHashMap(); - LinkedHashMap get previews => _items; + LinkedHashMap fileQueue = LinkedHashMap(); + final int _maxPreviewSizeLimitForCache = 50 * 1024 * 1024; // 50 MB Set? _failureFiles; - bool _initSuccess = false; + bool _hasQueuedFile = false; - PreviewVideoStore._privateConstructor(); + VideoPreviewService._privateConstructor(); - static final PreviewVideoStore instance = - PreviewVideoStore._privateConstructor(); + static final VideoPreviewService instance = + VideoPreviewService._privateConstructor(); - final _logger = Logger("PreviewVideoStore"); final cacheManager = DefaultCacheManager(); final videoCacheManager = VideoCacheManager.instance; - LinkedHashSet fileQueue = LinkedHashSet(); int uploadingFileId = -1; - final _dio = NetworkClient.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; + final _nonEnteDio = NetworkClient.instance.getDio(); + final CollectionsService collectionsService = CollectionsService.instance; void init(SharedPreferences prefs) { _prefs = prefs; @@ -86,7 +88,7 @@ class PreviewVideoStore { Bus.instance.fire(VideoStreamingChanged()); if (isVideoStreamingEnabled) { - await FileDataService.instance.syncFDStatus(); + await fileDataService.syncFDStatus(); _putFilesForPreviewCreation().ignore(); } else { clearQueue(); @@ -96,7 +98,7 @@ class PreviewVideoStore { void clearQueue() { fileQueue.clear(); _items.clear(); - Bus.instance.fire(PreviewUpdatedEvent(_items)); + _hasQueuedFile = false; } DateTime? get videoStreamingCutoff { @@ -105,12 +107,29 @@ class PreviewVideoStore { return DateTime.fromMillisecondsSinceEpoch(milliseconds); } + Future isSharedFileStreamble(EnteFile file) async { + try { + if (fileDataService.previewIds.containsKey(file.uploadedFileID)) { + return true; + } + await _getPreviewUrl(file); + return true; + } catch (_) { + return false; + } + } + Future chunkAndUploadVideo( BuildContext? ctx, EnteFile enteFile, [ bool forceUpload = false, ]) async { - if (!isVideoStreamingEnabled) { + if (!isVideoStreamingEnabled || + !computeController.requestCompute(stream: true)) { + _logger.info( + "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission)", + ); + if (isVideoStreamingEnabled) _logger.info("No permission to run compute"); clearQueue(); return; } @@ -122,17 +141,15 @@ class PreviewVideoStore { removeFile = true; return; } - try { // check if playlist already exist - await getPlaylist(enteFile); - final _ = await getPreviewUrl(enteFile); - - if (ctx != null && ctx.mounted) { - showShortToast(ctx, 'Video preview already exists'); + if (await getPlaylist(enteFile) != null) { + if (ctx != null && ctx.mounted) { + showShortToast(ctx, 'Video preview already exists'); + } + removeFile = true; + return; } - removeFile = true; - return; } catch (e, s) { if (e is DioException && e.response?.statusCode == 404) { _logger.info("No preview found for $enteFile"); @@ -142,7 +159,6 @@ class PreviewVideoStore { return; } } - // elimination case for <=10 MB with H.264 var (props, result, file) = await _checkFileForPreviewCreation(enteFile); if (result) { @@ -162,8 +178,7 @@ class PreviewVideoStore { : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); - fileQueue.add(enteFile); + fileQueue[enteFile.uploadedFileID!] = enteFile; return; } @@ -176,7 +191,6 @@ class PreviewVideoStore { forceUpload ? 0 : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); // get file file ??= await getFile(enteFile, isOrigin: true); @@ -284,7 +298,6 @@ class PreviewVideoStore { collectionID: enteFile.collectionID ?? 0, retryCount: _items[enteFile.uploadedFileID!]?.retryCount ?? 0, ); - Bus.instance.fire(PreviewUpdatedEvent(_items)); _logger.info('Playlist Generated ${enteFile.displayName}'); @@ -342,7 +355,7 @@ class PreviewVideoStore { if (error == null) { // update previewIds - FileDataService.instance.appendPreview( + fileDataService.appendPreview( enteFile.uploadedFileID!, objectId!, objectSize!, @@ -356,17 +369,14 @@ class PreviewVideoStore { ); _removeFromLocks(enteFile).ignore(); Directory(prefix).delete(recursive: true).ignore(); - - Bus.instance.fire(PreviewUpdatedEvent(_items)); } } finally { + computeController.releaseCompute(stream: true); if (error != null) { _retryFile(enteFile, error); - Bus.instance.fire(PreviewUpdatedEvent(_items)); } else if (removeFile) { _removeFile(enteFile); _removeFromLocks(enteFile).ignore(); - Bus.instance.fire(PreviewUpdatedEvent(_items)); } // reset uploading status if this was getting processed if (uploadingFileId == enteFile.uploadedFileID!) { @@ -375,8 +385,9 @@ class PreviewVideoStore { _logger.info("[chunk] Processing ${_items.length} items for streaming"); // process next file if (fileQueue.isNotEmpty) { - final file = fileQueue.first; - fileQueue.remove(file); + final entry = fileQueue.entries.first; + final file = entry.value; + fileQueue.remove(entry.key); await chunkAndUploadVideo(ctx, file); } } @@ -405,7 +416,7 @@ class PreviewVideoStore { retryCount: _items[enteFile.uploadedFileID!]!.retryCount + 1, collectionID: enteFile.collectionID ?? 0, ); - fileQueue.add(enteFile); + fileQueue[enteFile.uploadedFileID!] = enteFile; } else { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.failed, @@ -455,7 +466,7 @@ class PreviewVideoStore { }, encryptionKey, ); - final _ = await _dio.put( + final _ = await _enteDio.put( "/files/video-data", data: { "fileID": file.uploadedFileID!, @@ -474,7 +485,7 @@ class PreviewVideoStore { Future<(String, int)> _uploadPreviewVideo(EnteFile file, File preview) async { _logger.info("Pushing preview for $file"); try { - final response = await _dio.get( + final response = await _enteDio.get( "/files/data/preview-upload-url", queryParameters: { "fileID": file.uploadedFileID!, @@ -484,7 +495,7 @@ class PreviewVideoStore { final uploadURL = response.data["url"]; final String objectID = response.data["objectID"]; final objectSize = preview.lengthSync(); - final _ = await _dio.put( + final _ = await _enteDio.put( uploadURL, data: preview.openRead(), options: Options( @@ -520,16 +531,25 @@ class PreviewVideoStore { _logger.info("Getting playlist for $file"); int? width, height, size; try { - final objectKey = - FileDataService.instance.previewIds?[file.uploadedFileID!]?.objectId; - final FileInfo? playlistCache = (objectKey == null) - ? null - : await cacheManager.getFileFromCache(_getCacheKey(objectKey)); - final detailsCache = (objectKey == null) - ? null - : await cacheManager.getFileFromCache( - _getDetailsCacheKey(objectKey), - ); + late final String objectID; + final PreviewInfo? previewInfo = + fileDataService.previewIds[file.uploadedFileID!]; + bool shouldAppendPreview = false; + (String, String)? previewURLResult; + if (previewInfo == null) { + shouldAppendPreview = true; + previewURLResult = await _getPreviewUrl(file); + _logger.info("parrsed objectID: ${previewURLResult.$2}"); + objectID = previewURLResult.$2; + } else { + objectID = previewInfo.objectId; + } + + final FileInfo? playlistCache = + await cacheManager.getFileFromCache(_getCacheKey(objectID)); + final detailsCache = await cacheManager.getFileFromCache( + _getDetailsCacheKey(objectID), + ); String finalPlaylist; if (playlistCache != null) { finalPlaylist = playlistCache.file.readAsStringSync(); @@ -540,113 +560,167 @@ class PreviewVideoStore { size = details["size"]; } } else { - final response = await _dio.get( - "/files/data/fetch/", - queryParameters: { - "fileID": file.uploadedFileID, - "type": "vid_preview", - }, - ); - final encryptedData = response.data["data"]["encryptedData"]; - final header = response.data["data"]["decryptionHeader"]; - final encryptionKey = getFileKey(file); - final playlistData = await decryptAndUnzipJson( - encryptionKey, - encryptedData: encryptedData, - header: header, - ); + final Map playlistData = await _getPlaylistData(file); finalPlaylist = playlistData["playlist"]; - width = playlistData["width"]; height = playlistData["height"]; size = playlistData["size"]; - - if (objectKey != null) { - unawaited( - cacheManager.putFile( - _getCacheKey(objectKey), - Uint8List.fromList( - (playlistData["playlist"] as String).codeUnits, - ), + unawaited( + cacheManager.putFile( + _getCacheKey(objectID), + Uint8List.fromList( + (playlistData["playlist"] as String).codeUnits, ), - ); - final details = { - "width": width, - "height": height, - "size": size, - }; - unawaited( - cacheManager.putFile( - _getDetailsCacheKey(objectKey), - Uint8List.fromList( - json.encode(details).codeUnits, - ), - ), - ); - } - } - - final videoFile = objectKey == null - ? null - : (await videoCacheManager - .getFileFromCache(_getVideoPreviewKey(objectKey))) - ?.file; - if (videoFile == null) { - final response2 = await _dio.get( - "/files/data/preview", - queryParameters: { - "fileID": file.uploadedFileID, - "type": "vid_preview", - }, + ), ); - final previewURL = response2.data["url"]; - if (objectKey != null) { + unawaited( + cacheManager.putFile( + _getDetailsCacheKey(objectID), + Uint8List.fromList( + json.encode({ + "width": width, + "height": height, + "size": size, + }).codeUnits, + ), + ), + ); + } + final videoFile = (await videoCacheManager + .getFileFromCache(_getVideoPreviewKey(objectID))) + ?.file; + if (videoFile == null) { + previewURLResult = previewURLResult ?? await _getPreviewUrl(file); + if (size != null && size < _maxPreviewSizeLimitForCache) { unawaited( - _downloadAndCacheVideo( - previewURL, - _getVideoPreviewKey(objectKey), + videoCacheManager.downloadFile( + previewURLResult.$1, + key: _getVideoPreviewKey(objectID), ), ); } finalPlaylist = - finalPlaylist.replaceAll('\noutput.ts', '\n$previewURL'); + finalPlaylist.replaceAll('\noutput.ts', '\n${previewURLResult.$1}'); } else { finalPlaylist = finalPlaylist.replaceAll('\noutput.ts', '\n${videoFile.path}'); } - final tempDir = await getTemporaryDirectory(); final playlistFile = File("${tempDir.path}/${file.uploadedFileID}.m3u8"); await playlistFile.writeAsString(finalPlaylist); - _logger.info("Writing playlist to ${playlistFile.path}"); + final String log = (StringBuffer() + ..write("[CACHE-STATUS] ") + ..write("Video: ${videoFile != null ? '✓' : '✗'} | ") + ..write("Details: ${detailsCache != null ? '✓' : '✗'} | ") + ..write("Playlist: ${playlistCache != null ? '✓' : '✗'}")) + .toString(); + _logger.info("Mapped playlist to ${playlistFile.path}, $log"); final data = PlaylistData( preview: playlistFile, width: width, height: height, size: size, + durationInSeconds: parseDurationFromHLS(finalPlaylist), ); + if (shouldAppendPreview) { + fileDataService.appendPreview( + file.uploadedFileID!, + objectID, + size!, + ); + } return data; } catch (_) { rethrow; } } - Future _downloadAndCacheVideo(String url, String key) async { - final file = await videoCacheManager.downloadFile(url, key: key); - return file; - } - - Future getPreviewUrl(EnteFile file) async { - try { - final response = await _dio.get( - "/files/data/preview", + Future> _getPlaylistData(EnteFile file) async { + late Response response; + if (collectionsService.isSharedPublicLink(file.collectionID!)) { + response = await _nonEnteDio.get( + "${Configuration.instance.getHttpEndpoint()}/public-collection/files/data/fetch/", queryParameters: { "fileID": file.uploadedFileID, - "type": - file.fileType == FileType.video ? "vid_preview" : "img_preview", + "type": "vid_preview", + }, + options: Options( + headers: + collectionsService.publicCollectionHeaders(file.collectionID!), + ), + ); + } else { + response = await _enteDio.get( + "/files/data/fetch/", + queryParameters: { + "fileID": file.uploadedFileID, + "type": "vid_preview", }, ); - return response.data["url"]; + } + final encryptedData = response.data["data"]["encryptedData"]; + final header = response.data["data"]["decryptionHeader"]; + final encryptionKey = getFileKey(file); + final playlistData = await decryptAndUnzipJson( + encryptionKey, + encryptedData: encryptedData, + header: header, + ); + return playlistData; + } + + int? parseDurationFromHLS(String playlist) { + final lines = playlist.split("\n"); + double totalDuration = 0.0; + for (final line in lines) { + if (line.startsWith("#EXTINF:")) { + // Extract duration value (e.g., "#EXTINF:2.400000," → "2.400000") + final durationStr = line.substring( + 8, + line.length - 1, + ); + final duration = double.tryParse(durationStr); + if (duration != null) { + totalDuration += duration; + } + } + } + return totalDuration > 0 ? totalDuration.round() : null; + } + + Future<(String, String)> _getPreviewUrl(EnteFile file) async { + try { + late String url; + if (collectionsService.isSharedPublicLink(file.collectionID!)) { + final response = await _nonEnteDio.get( + "${Configuration.instance.getHttpEndpoint()}/public-collection/files/data/preview", + queryParameters: { + "fileID": file.uploadedFileID, + "type": + file.fileType == FileType.video ? "vid_preview" : "img_preview", + }, + options: Options( + headers: + collectionsService.publicCollectionHeaders(file.collectionID!), + ), + ); + url = (response.data["url"] as String); + } else { + final response = await _enteDio.get( + "/files/data/preview", + queryParameters: { + "fileID": file.uploadedFileID, + "type": + file.fileType == FileType.video ? "vid_preview" : "img_preview", + }, + ); + url = (response.data["url"] as String); + } + final uri = Uri.parse(url); + final segments = uri.pathSegments; + if (segments.isEmpty) throw Exception("Invalid URL"); + final String objectID = segments.last; + return (url, objectID); } catch (e) { _logger.warning("Failed to get preview url", e); rethrow; @@ -656,20 +730,37 @@ class PreviewVideoStore { Future<(FFProbeProps?, bool, File?)> _checkFileForPreviewCreation( EnteFile enteFile, ) async { - final fileSize = enteFile.fileSize; + if ((enteFile.pubMagicMetadata?.sv ?? 0) == 1) { + _logger.info( + "Skip Preview due to sv=1 for ${enteFile.displayName}", + ); + return (null, true, null); + } + if (enteFile.fileSize == null || enteFile.duration == null) { + _logger.warning( + "Skip Preview due to misisng size/duration for ${enteFile.displayName}", + ); + return (null, true, null); + } + final int size = enteFile.fileSize!; + final int duration = enteFile.duration!; + if (size >= 500 * 1024 * 1024 || duration > 60) { + _logger.info( + "Skip Preview due to size: $size or duration: $duration", + ); + return (null, true, null); + } FFProbeProps? props; File? file; bool skipFile = false; - try { - final isFileUnder10MB = fileSize != null && fileSize <= 10 * 1024 * 1024; + final isFileUnder10MB = size <= 10 * 1024 * 1024; if (isFileUnder10MB) { file = await getFile(enteFile, isOrigin: true); if (file != null) { props = await getVideoPropsAsync(file); final videoData = List.from(props?.propData?["streams"] ?? []) .firstWhereOrNull((e) => e["type"] == "video"); - final codec = videoData["codec_name"]?.toString().toLowerCase(); skipFile = codec?.contains("h264") ?? false; @@ -681,28 +772,6 @@ class PreviewVideoStore { } } } - - int? size = enteFile.fileSize; - int? duration = enteFile.duration; - - if (size == null) { - file = await getFile(enteFile, isOrigin: true); - size = file?.lengthSync(); - } - - if (duration == null) { - file ??= await getFile(enteFile, isOrigin: true); - props = await getVideoPropsAsync(file!); - duration = props?.duration?.inSeconds; - } - - if ((size == null || duration == null) || - (size >= 500 * 1024 * 1024 || duration > 60)) { - skipFile = true; - _logger.info( - "[init] Ignoring file ${enteFile.displayName} for preview due to size: $size and duration: $duration", - ); - } } catch (e, sT) { _logger.warning("Failed to check props", e, sT); } @@ -715,7 +784,7 @@ class PreviewVideoStore { final cutoff = videoStreamingCutoff; if (cutoff == null) return; - if (updateInit) _initSuccess = true; + if (updateInit) _hasQueuedFile = true; Map failureFiles = {}; try { @@ -724,7 +793,7 @@ class PreviewVideoStore { // handle case when failures are already previewed for (final failure in _failureFiles!) { - if (previews.containsKey(failure)) { + if (_items.containsKey(failure)) { UploadLocksDB.instance.deleteStreamUploadErrorEntry(failure).ignore(); } } @@ -736,28 +805,16 @@ class PreviewVideoStore { userID: Configuration.instance.getUserID()!, ); - final previewIds = FileDataService.instance.previewIds; - final allFiles = files - .where((file) => previewIds?[file.uploadedFileID] == null) - .sorted((a, b) { - // put higher duration videos last along with remote files - final first = (a.localID == null ? 2 : 0) + - (a.duration == null || a.duration! >= 10 * 60 ? 1 : 0); - final second = (b.localID == null ? 2 : 0) + - (b.duration == null || b.duration! >= 10 * 60 ? 1 : 0); - return first.compareTo(second); - }).toList(); + final previewIds = fileDataService.previewIds; + final allFiles = + files.where((file) => previewIds[file.uploadedFileID] == null).toList(); // set all video status to in queue var n = allFiles.length, i = 0; while (i < n) { final enteFile = allFiles[i]; - - // elimination case for <=10 MB with H.264 - final (_, result, _) = await _checkFileForPreviewCreation(enteFile); final isFailure = _failureFiles?.contains(enteFile.uploadedFileID!) ?? false; - if (isFailure) { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.failed, @@ -767,7 +824,7 @@ class PreviewVideoStore { error: failureFiles[enteFile.uploadedFileID!], ); } - if (result || isFailure) { + if (isFailure) { _logger.info( "[init] Ignoring file ${enteFile.displayName} for preview", ); @@ -785,7 +842,6 @@ class PreviewVideoStore { i++; } - Bus.instance.fire(PreviewUpdatedEvent(_items)); if (allFiles.isEmpty) { _logger.info("[init] No preview to cache"); return; @@ -795,15 +851,22 @@ class PreviewVideoStore { // take first file and put it for stream generation final file = allFiles.removeAt(0); - fileQueue.addAll(allFiles); + for (final enteFile in allFiles) { + if (_items.containsKey(enteFile.uploadedFileID!)) { + continue; + } + fileQueue[enteFile.uploadedFileID!] = enteFile; + } chunkAndUploadVideo(null, file).ignore(); } void queueFiles() { - if (!_initSuccess) { - _putFilesForPreviewCreation(true).catchError((_) { - _initSuccess = false; - }); - } + Future.delayed(const Duration(seconds: 5), () { + if (!_hasQueuedFile && computeController.requestCompute(stream: true)) { + _putFilesForPreviewCreation(true).catchError((_) { + _hasQueuedFile = false; + }); + } + }); } } diff --git a/mobile/lib/states/all_sections_examples_state.dart b/mobile/lib/states/all_sections_examples_state.dart index 19e640d318..2a9482f63f 100644 --- a/mobile/lib/states/all_sections_examples_state.dart +++ b/mobile/lib/states/all_sections_examples_state.dart @@ -96,9 +96,16 @@ class _AllSectionsExamplesProviderState _logger.info("'_debounceTimer: reloading all sections in search tab"); final allSectionsExamples = >>[]; for (SectionType sectionType in SectionType.values) { - allSectionsExamples.add( - sectionType.getData(context, limit: kSearchSectionLimit), - ); + // Contacts section have been moved to shared collections tab + // temporarily from search tab. So we can skip computing data here + // since 'allSectionsExamples' is for search tab sections only. + if (sectionType == SectionType.contacts) { + allSectionsExamples.add(Future.value([])); + } else { + allSectionsExamples.add( + sectionType.getData(context, limit: kSearchSectionLimit), + ); + } } try { allSectionsExamplesFuture = Future.wait>( diff --git a/mobile/lib/ui/account/change_email_dialog.dart b/mobile/lib/ui/account/change_email_dialog.dart index 3f0c83d9c1..974ad17a6f 100644 --- a/mobile/lib/ui/account/change_email_dialog.dart +++ b/mobile/lib/ui/account/change_email_dialog.dart @@ -18,7 +18,7 @@ class _ChangeEmailDialogState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return AlertDialog( - title: Text(l10n.enterYourEmailAddress), + title: Text(l10n.enterYourNewEmailAddress), content: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/mobile/lib/ui/account/recovery_key_page.dart b/mobile/lib/ui/account/recovery_key_page.dart index 1d4d9a57d7..e1c13780c1 100644 --- a/mobile/lib/ui/account/recovery_key_page.dart +++ b/mobile/lib/ui/account/recovery_key_page.dart @@ -249,7 +249,11 @@ class _RecoveryKeyPageState extends State { } _recoveryKeyFile.writeAsStringSync(recoveryKey); - await Share.shareXFiles([XFile(_recoveryKeyFile.path)]); + await SharePlus.instance.share( + ShareParams( + files: [XFile(_recoveryKeyFile.path)], + ), + ); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { setState(() { diff --git a/mobile/lib/ui/account/sessions_page.dart b/mobile/lib/ui/account/sessions_page.dart index ed3c48407b..40fc8453c8 100644 --- a/mobile/lib/ui/account/sessions_page.dart +++ b/mobile/lib/ui/account/sessions_page.dart @@ -83,7 +83,7 @@ class _SessionsPageState extends State { color: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.8), + .withValues(alpha: 0.8), fontSize: 14, ), ), @@ -96,7 +96,7 @@ class _SessionsPageState extends State { color: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.8), + .withValues(alpha: 0.8), fontSize: 12, ), ), diff --git a/mobile/lib/ui/account/two_factor_recovery_page.dart b/mobile/lib/ui/account/two_factor_recovery_page.dart index 8beea1da75..8ce492475a 100644 --- a/mobile/lib/ui/account/two_factor_recovery_page.dart +++ b/mobile/lib/ui/account/two_factor_recovery_page.dart @@ -101,8 +101,9 @@ class _TwoFactorRecoveryPageState extends State { style: TextStyle( decoration: TextDecoration.underline, fontSize: 12, - color: - getEnteColorScheme(context).textBase.withOpacity(0.9), + color: getEnteColorScheme(context) + .textBase + .withValues(alpha: 0.9), ), ), ), diff --git a/mobile/lib/ui/account/two_factor_setup_page.dart b/mobile/lib/ui/account/two_factor_setup_page.dart index 5817b4afed..b1eb09d4a8 100644 --- a/mobile/lib/ui/account/two_factor_setup_page.dart +++ b/mobile/lib/ui/account/two_factor_setup_page.dart @@ -160,14 +160,14 @@ class _TwoFactorSetupPageState extends State padding: const EdgeInsets.only(left: 10, right: 10), child: Container( padding: const EdgeInsets.all(16), - color: textColor.withOpacity(0.1), + color: textColor.withValues(alpha: 0.1), child: Center( child: Text( widget.secretCode, style: TextStyle( fontSize: 15, fontFeatures: const [FontFeature.tabularFigures()], - color: textColor.withOpacity(0.7), + color: textColor.withValues(alpha: 0.7), ), ), ), @@ -176,7 +176,7 @@ class _TwoFactorSetupPageState extends State const Padding(padding: EdgeInsets.all(6)), Text( S.of(context).tapToCopy, - style: TextStyle(color: textColor.withOpacity(0.5)), + style: TextStyle(color: textColor.withValues(alpha: 0.5)), ), ], ), diff --git a/mobile/lib/ui/actions/collection/collection_file_actions.dart b/mobile/lib/ui/actions/collection/collection_file_actions.dart index cf28e6687c..2deb10cf2a 100644 --- a/mobile/lib/ui/actions/collection/collection_file_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_file_actions.dart @@ -84,6 +84,104 @@ extension CollectionFileActions on CollectionActions { } } + Future addToMultipleCollections( + BuildContext context, + List collections, + bool showProgressDialog, { + List? selectedFiles, + }) async { + final ProgressDialog? dialog = showProgressDialog + ? createProgressDialog( + context, + S.of(context).uploadingFilesToAlbum, + isDismissible: true, + ) + : null; + await dialog?.show(); + final int currentUserID = Configuration.instance.getUserID()!; + for (final collection in collections) { + try { + final List files = []; + final List filesPendingUpload = []; + for (final file in selectedFiles!) { + EnteFile? currentFile; + if (file.uploadedFileID != null) { + currentFile = file.copyWith(); + } else if (file.generatedID != null) { + // when file is not uploaded, refresh the state from the db to + // ensure we have latest upload status for given file before + // queueing it up as pending upload + currentFile = await (FilesDB.instance.getFile(file.generatedID!)); + } else if (file.generatedID == null) { + logger.severe("generated id should not be null"); + } + if (currentFile == null) { + logger.severe("Failed to find fileBy genID"); + continue; + } + + if (currentFile.uploadedFileID == null) { + currentFile.collectionID = collection.id; + filesPendingUpload.add(currentFile); + } else { + files.add(currentFile); + } + } + if (filesPendingUpload.isNotEmpty) { + // Newly created collection might not be cached + final Collection? c = + CollectionsService.instance.getCollectionByID(collection.id); + if (c != null && c.owner.id != currentUserID) { + final Collection uncat = + await CollectionsService.instance.getUncategorizedCollection(); + for (EnteFile unuploadedFile in filesPendingUpload) { + final uploadedFile = await FileUploader.instance.forceUpload( + unuploadedFile, + uncat.id, + ); + files.add(uploadedFile); + } + } else { + for (final file in filesPendingUpload) { + file.collectionID = collection.id; + } + // filesPendingUpload might be getting ignored during auto-upload + // because the user deleted these files from ente in the past. + await IgnoredFilesService.instance + .removeIgnoredMappings(filesPendingUpload); + await FilesDB.instance.insertMultiple(filesPendingUpload); + Bus.instance.fire( + CollectionUpdatedEvent( + collection.id, + filesPendingUpload, + "pendingFilesAdd", + ), + ); + } + } + if (files.isNotEmpty) { + await CollectionsService.instance + .addOrCopyToCollection(collection.id, files); + } + } catch (e, s) { + logger.severe("Failed to add to album", e, s); + await dialog?.hide(); + await showGenericErrorDialog( + context: context, + error: e, + ); + return false; + } finally { + // Syncing since successful addition to collection could have + // happened before a failure + unawaited(RemoteSyncService.instance.sync(silently: true)); + } + } + + await dialog?.hide(); + return true; + } + Future addToCollection( BuildContext context, int collectionID, diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index 87b552269d..0583798dcf 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -333,6 +333,90 @@ class CollectionActions { } } + Future deleteMultipleCollectionSheet( + BuildContext bContext, + List collections, + ) async { + final textTheme = getEnteTextTheme(bContext); + final actionResult = await showActionSheet( + context: bContext, + buttons: [ + ButtonWidget( + labelText: S.of(bContext).keepPhotos, + buttonType: ButtonType.neutral, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + shouldStickToDarkTheme: true, + isInAlert: true, + onTap: () async { + for (final collection in collections) { + try { + await trashCollectionKeepingPhotos(collection, bContext); + } catch (e, s) { + logger.severe( + "Failed to keep photos & delete collection", + e, + s, + ); + rethrow; + } + } + }, + ), + ButtonWidget( + labelText: S.of(bContext).deletePhotos, + buttonType: ButtonType.critical, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + isInAlert: true, + onTap: () async { + for (final collection in collections) { + try { + await collectionsService.trashNonEmptyCollection(collection); + } catch (e) { + logger.severe("Failed to delete collection", e); + rethrow; + } + } + }, + ), + ButtonWidget( + labelText: S.of(bContext).cancel, + buttonType: ButtonType.secondary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + bodyWidget: StyledText( + text: S.of(bContext).deleteMultipleAlbumDialog(collections.length), + style: textTheme.body.copyWith(color: textMutedDark), + tags: { + 'bold': StyledTextTag( + style: textTheme.body.copyWith(color: textBaseDark), + ), + }, + ), + actionSheetType: ActionSheetType.defaultActionSheet, + ); + if (actionResult?.action != null && + actionResult!.action == ButtonAction.error) { + await showGenericErrorDialog( + context: bContext, + error: actionResult.exception, + ); + return false; + } + if ((actionResult?.action != null) && + (actionResult!.action == ButtonAction.first || + actionResult.action == ButtonAction.second)) { + return true; + } + return false; + } + // deleteCollectionSheet returns true if the album is successfully deleted Future deleteCollectionSheet( BuildContext bContext, diff --git a/mobile/lib/ui/collections/album/column_item.dart b/mobile/lib/ui/collections/album/column_item.dart index a81d71273e..bf793d7214 100644 --- a/mobile/lib/ui/collections/album/column_item.dart +++ b/mobile/lib/ui/collections/album/column_item.dart @@ -6,16 +6,19 @@ import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/services/collections_service.dart"; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; ///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7480%3A33462&t=H5AvR79OYDnB9ekw-4 class AlbumColumnItemWidget extends StatelessWidget { final Collection collection; + final List selectedCollections; const AlbumColumnItemWidget( this.collection, { super.key, + this.selectedCollections = const [], }); @override @@ -23,12 +26,25 @@ class AlbumColumnItemWidget extends StatelessWidget { final textTheme = getEnteTextTheme(context); final colorScheme = getEnteColorScheme(context); const sideOfThumbnail = 60.0; - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - alignment: Alignment.center, - children: [ - Row( + final isSelected = selectedCollections.contains(collection); + return AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + border: Border.all( + color: + isSelected ? colorScheme.strokeMuted : colorScheme.strokeFainter, + ), + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 6, + child: Row( children: [ ClipRRect( borderRadius: const BorderRadius.horizontal( @@ -57,62 +73,72 @@ class AlbumColumnItemWidget extends StatelessWidget { ), ), ), - Padding( - padding: const EdgeInsets.only(left: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(collection.displayName), - FutureBuilder( - future: CollectionsService.instance.getFileCount - (collection), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - S.of(context).memoryCount( - snapshot.data!, - NumberFormat().format(snapshot.data!), - ), - style: textTheme.small.copyWith( - color: colorScheme.textMuted, - ), - ); - } else { - if (snapshot.hasError) { - Logger("AlbumListItemWidget").severe( - "Failed to fetch file count of collection", - snapshot.error, + const SizedBox(width: 12), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + collection.displayName, + overflow: TextOverflow.ellipsis, + ), + FutureBuilder( + future: CollectionsService.instance + .getFileCount(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + S.of(context).memoryCount( + snapshot.data!, + NumberFormat().format(snapshot.data!), + ), + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } else { + if (snapshot.hasError) { + Logger("AlbumListItemWidget").severe( + "Failed to fetch file count of collection", + snapshot.error, + ); + } + return Text( + "", + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), ); } - return Text( - "", - style: textTheme.small.copyWith( - color: colorScheme.textMuted, - ), - ); - } - }, - ), - ], + }, + ), + ], + ), ), ), ], ), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - border: Border.all( - color: colorScheme.strokeFainter, - ), - ), - height: sideOfThumbnail, - width: constraints.maxWidth, - ), + ), + Flexible( + flex: 1, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? IconButtonWidget( + key: const ValueKey("selected"), + icon: Icons.check_circle_rounded, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokeBase, + ) + : null, ), - ], - ); - }, + ), + ], + ), ); } } diff --git a/mobile/lib/ui/collections/album/list_item.dart b/mobile/lib/ui/collections/album/list_item.dart new file mode 100644 index 0000000000..255d6de147 --- /dev/null +++ b/mobile/lib/ui/collections/album/list_item.dart @@ -0,0 +1,160 @@ +import "package:flutter/material.dart"; +import "package:intl/intl.dart"; +import "package:logging/logging.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; +import "package:photos/ui/viewer/file/thumbnail_widget.dart"; + +class AlbumListItemWidget extends StatelessWidget { + final Collection collection; + final void Function(Collection)? onTapCallback; + final void Function(Collection)? onLongPressCallback; + final SelectedAlbums? selectedAlbums; + + const AlbumListItemWidget( + this.collection, { + super.key, + this.onTapCallback, + this.onLongPressCallback, + this.selectedAlbums, + }); + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + const sideOfThumbnail = 60.0; + + final albumWidget = Flexible( + flex: 6, + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(4), + ), + child: SizedBox( + height: sideOfThumbnail, + width: sideOfThumbnail, + child: FutureBuilder( + future: CollectionsService.instance.getCover(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + final thumbnail = snapshot.data!; + return ThumbnailWidget( + thumbnail, + showFavForAlbumOnly: true, + shouldShowOwnerAvatar: false, + ); + } else { + return const NoThumbnailWidget(addBorder: false); + } + }, + ), + ), + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + collection.displayName, + overflow: TextOverflow.ellipsis, + ), + FutureBuilder( + future: CollectionsService.instance.getFileCount(collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + S.of(context).memoryCount( + snapshot.data!, + NumberFormat().format(snapshot.data!), + ), + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } else { + if (snapshot.hasError) { + Logger("AlbumListItemWidget").severe( + "Failed to fetch file count of collection", + snapshot.error, + ); + } + return Text( + "", + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } + }, + ), + ], + ), + ), + ], + ), + ); + + return GestureDetector( + onTap: () => onTapCallback?.call(collection), + onLongPress: () => onLongPressCallback?.call(collection), + behavior: HitTestBehavior.opaque, + child: ListenableBuilder( + listenable: selectedAlbums!, + builder: (context, _) { + final isSelected = + selectedAlbums?.isAlbumSelected(collection) ?? false; + + return AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? colorScheme.strokeMuted + : colorScheme.strokeFainter, + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + albumWidget, + Flexible( + flex: 1, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? IconButtonWidget( + key: const ValueKey("selected"), + icon: Icons.check_circle_rounded, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokeBase, + ) + : IconButtonWidget( + key: const ValueKey("unselected"), + icon: Icons.chevron_right_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.blurStrokePressed, + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/mobile/lib/ui/collections/album/new_list_item.dart b/mobile/lib/ui/collections/album/new_list_item.dart index 4274fafcc8..735946f223 100644 --- a/mobile/lib/ui/collections/album/new_list_item.dart +++ b/mobile/lib/ui/collections/album/new_list_item.dart @@ -47,7 +47,7 @@ class NewAlbumListItemWidget extends StatelessWidget { IgnorePointer( child: DottedBorder( dashPattern: const [4], - color: colorScheme.strokeFainter, + color: colorScheme.strokeFaint, strokeWidth: 1, padding: const EdgeInsets.all(0), borderType: BorderType.RRect, diff --git a/mobile/lib/ui/collections/new_album_icon.dart b/mobile/lib/ui/collections/album/new_row_item.dart similarity index 53% rename from mobile/lib/ui/collections/new_album_icon.dart rename to mobile/lib/ui/collections/album/new_row_item.dart index 4e18123b66..82f39b95c5 100644 --- a/mobile/lib/ui/collections/new_album_icon.dart +++ b/mobile/lib/ui/collections/album/new_row_item.dart @@ -1,30 +1,29 @@ +import "package:dotted_border/dotted_border.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/services/collections_service.dart"; -import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; -class NewAlbumIcon extends StatelessWidget { - final IconData icon; +class NewAlbumRowItemWidget extends StatelessWidget { final Color? color; - final IconButtonType iconButtonType; - const NewAlbumIcon({ - required this.icon, - required this.iconButtonType, + final double height; + final double width; + const NewAlbumRowItemWidget({ this.color, super.key, + required this.height, + required this.width, }); @override Widget build(BuildContext context) { - return IconButtonWidget( - icon: icon, - iconButtonType: iconButtonType, + return GestureDetector( onTap: () async { final result = await showTextInputDialog( context, @@ -34,23 +33,27 @@ class NewAlbumIcon extends StatelessWidget { alwaysShowSuccessState: false, initialValue: "", textCapitalization: TextCapitalization.words, - popnavAfterSubmission: false, + popnavAfterSubmission: true, onSubmit: (String text) async { - if (text.trim() == "") { + text = text.trim(); + if (text == "") { return; } try { final Collection c = await CollectionsService.instance.createAlbum(text); + + // Close the dialog now so that it does not flash when leaving the album again. + Navigator.of(context).pop(); + // ignore: unawaited_futures await routeToPage( context, CollectionPage(CollectionWithThumbnail(c, null)), ); - Navigator.of(context).pop(); } catch (e, s) { - Logger("CreateNewAlbumIcon") + Logger("CreateNewAlbumRowItemWidget") .severe("Failed to rename album", e, s); rethrow; } @@ -61,6 +64,34 @@ class NewAlbumIcon extends StatelessWidget { await showGenericErrorDialog(context: context, error: result); } }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: height, + width: width, + child: DottedBorder( + borderType: BorderType.RRect, + strokeWidth: 1.5, + dashPattern: const [3.75, 3.75], + radius: const Radius.circular(2.35), + padding: EdgeInsets.zero, + color: getEnteColorScheme(context).strokeMuted, + child: Center( + child: Icon( + Icons.add, + color: getEnteColorScheme(context).strokeMuted, + ), + ), + ), + ), + const SizedBox(height: 4), + Text( + S.of(context).addNew, + style: getEnteTextTheme(context).smallFaint, + ), + ], + ), ); } } diff --git a/mobile/lib/ui/collections/album/row_item.dart b/mobile/lib/ui/collections/album/row_item.dart index 5f684d3780..490a43d8be 100644 --- a/mobile/lib/ui/collections/album/row_item.dart +++ b/mobile/lib/ui/collections/album/row_item.dart @@ -4,6 +4,7 @@ import "package:photos/core/configuration.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; +import "package:photos/models/selected_albums.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; @@ -20,6 +21,9 @@ class AlbumRowItemWidget extends StatelessWidget { final bool showFileCount; final String tag; final bool? hasVerifiedLock; + final void Function(Collection)? onTapCallback; + final void Function(Collection)? onLongPressCallback; + final SelectedAlbums? selectedAlbums; const AlbumRowItemWidget( this.c, @@ -28,6 +32,9 @@ class AlbumRowItemWidget extends StatelessWidget { this.showFileCount = true, this.tag = "", this.hasVerifiedLock, + this.onTapCallback, + this.onLongPressCallback, + this.selectedAlbums, }); @override @@ -44,6 +51,7 @@ class AlbumRowItemWidget extends StatelessWidget { color: c.publicURLs.first.isExpired ? warning500 : strokeBaseDark, ) : null; + return GestureDetector( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -70,20 +78,33 @@ class AlbumRowItemWidget extends StatelessWidget { CollectionsService.instance.getCoverCache(c); } if (thumbnail != null) { + final bool isSelected = + selectedAlbums?.isAlbumSelected(c) ?? false; final String heroTag = tagPrefix + thumbnail.tag; + final thumbnailWidget = ThumbnailWidget( + thumbnail, + shouldShowArchiveStatus: isOwner + ? c.isArchived() + : c.hasShareeArchived(), + showFavForAlbumOnly: true, + shouldShowSyncStatus: false, + shouldShowPinIcon: isOwner && c.isPinned, + key: Key(heroTag), + ); return Hero( tag: heroTag, transitionOnUserGestures: true, - child: ThumbnailWidget( - thumbnail, - shouldShowArchiveStatus: isOwner - ? c.isArchived() - : c.hasShareeArchived(), - showFavForAlbumOnly: true, - shouldShowSyncStatus: false, - shouldShowPinIcon: isOwner && c.isPinned, - key: Key(heroTag), - ), + child: isSelected + ? ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withValues( + alpha: 0.4, + ), + BlendMode.darken, + ), + child: thumbnailWidget, + ) + : thumbnailWidget, ); } else { return const NoThumbnailWidget(); @@ -97,12 +118,40 @@ class AlbumRowItemWidget extends StatelessWidget { child: Align( alignment: Alignment.topLeft, child: AlbumSharesIcons( + padding: const EdgeInsets.only(left: 4, top: 4), sharees: c.getSharees(), type: AvatarType.mini, trailingWidget: linkIcon, ), ), ), + Positioned( + top: 5, + right: 5, + child: Hero( + tag: tagPrefix + "_album_selection", + transitionOnUserGestures: true, + child: ListenableBuilder( + listenable: selectedAlbums ?? ValueNotifier(false), + builder: (context, _) { + final bool isSelected = + selectedAlbums?.isAlbumSelected(c) ?? false; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? const Icon( + Icons.check_circle_rounded, + color: Colors.white, + size: 22, + ) + : null, + ); + }, + ), + ), + ), if (!isOwner) Align( alignment: Alignment.bottomRight, @@ -110,10 +159,8 @@ class AlbumRowItemWidget extends StatelessWidget { tag: tagPrefix + "_owner_other", transitionOnUserGestures: true, child: Padding( - padding: const EdgeInsets.only( - right: 8.0, - bottom: 8.0, - ), + padding: + const EdgeInsets.only(right: 4, bottom: 4), child: UserAvatarWidget( c.owner, thumbnailView: true, @@ -151,12 +198,12 @@ class AlbumRowItemWidget extends StatelessWidget { } if (cachedCount != null && cachedCount > 0) { final String textCount = NumberFormat().format(cachedCount); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( constraints: BoxConstraints( - maxWidth: - sideOfThumbnail - ((textCount.length + 3) * 10), + maxWidth: sideOfThumbnail, ), child: Text( c.displayName, @@ -164,11 +211,12 @@ class AlbumRowItemWidget extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), + const SizedBox(height: 2), RichText( text: TextSpan( - style: enteTextTheme.smallMuted, + style: enteTextTheme.tinyMuted, children: [ - TextSpan(text: ' \u2022 $textCount'), + TextSpan(text: textCount), ], ), ), @@ -188,6 +236,10 @@ class AlbumRowItemWidget extends StatelessWidget { ], ), onTap: () async { + if (onTapCallback != null) { + onTapCallback!(c); + return; + } final thumbnail = await CollectionsService.instance.getCover(c); // ignore: unawaited_futures routeToPage( @@ -199,6 +251,11 @@ class AlbumRowItemWidget extends StatelessWidget { ), ); }, + onLongPress: () { + if (onLongPressCallback != null) { + onLongPressCallback!(c); + } + }, ); } } diff --git a/mobile/lib/ui/collections/album/vertical_list.dart b/mobile/lib/ui/collections/album/vertical_list.dart index 78bcdc8b2f..d0f07e3c7c 100644 --- a/mobile/lib/ui/collections/album/vertical_list.dart +++ b/mobile/lib/ui/collections/album/vertical_list.dart @@ -1,6 +1,7 @@ import "dart:async"; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/core/event_bus.dart"; @@ -25,63 +26,96 @@ import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -class AlbumVerticalListWidget extends StatelessWidget { +class AlbumVerticalListWidget extends StatefulWidget { final List collections; final CollectionActionType actionType; final SelectedFiles? selectedFiles; final List? sharedFiles; final String searchQuery; final bool shouldShowCreateAlbum; + final bool enableSelection; + final List selectedCollections; + final Function()? onSelectionChanged; - AlbumVerticalListWidget( + const AlbumVerticalListWidget( this.collections, this.actionType, this.selectedFiles, this.sharedFiles, this.searchQuery, this.shouldShowCreateAlbum, { + required this.selectedCollections, + this.enableSelection = false, + this.onSelectionChanged, super.key, }); + @override + State createState() => + _AlbumVerticalListWidgetState(); +} + +class _AlbumVerticalListWidgetState extends State { final _logger = Logger("CollectionsListWidgetState"); + final CollectionActions _collectionActions = CollectionActions(CollectionsService.instance); @override Widget build(BuildContext context) { - final filesCount = sharedFiles != null - ? sharedFiles!.length - : selectedFiles?.files.length ?? 0; + final filesCount = widget.sharedFiles != null + ? widget.sharedFiles!.length + : widget.selectedFiles?.files.length ?? 0; - if (collections.isEmpty) { - if (shouldShowCreateAlbum) { + if (widget.collections.isEmpty) { + if (widget.shouldShowCreateAlbum) { return _getNewAlbumWidget(context, filesCount); } return const EmptyState(); } return ListView.separated( itemBuilder: (context, index) { - if (index == 0 && shouldShowCreateAlbum) { + if (index == 0 && widget.shouldShowCreateAlbum) { return _getNewAlbumWidget(context, filesCount); } - final item = collections[index - (shouldShowCreateAlbum ? 1 : 0)]; + final item = + widget.collections[index - (widget.shouldShowCreateAlbum ? 1 : 0)]; return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _albumListItemOnTap(context, item), + onTap: () => widget.enableSelection + ? _toggleCollectionSelection(item) + : _albumListItemOnTap(context, item), child: AlbumColumnItemWidget( item, + selectedCollections: widget.selectedCollections, ), ); }, - separatorBuilder: (context, index) => const SizedBox( - height: 8, - ), - itemCount: collections.length + (shouldShowCreateAlbum ? 1 : 0), - shrinkWrap: true, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: + widget.collections.length + (widget.shouldShowCreateAlbum ? 1 : 0), + shrinkWrap: false, physics: const BouncingScrollPhysics(), ); } + Future _toggleCollectionSelection( + Collection collection, + ) async { + if (widget.selectedCollections.contains(collection)) { + widget.selectedCollections.remove(collection); + } else { + widget.selectedCollections.isEmpty + ? await HapticFeedback.vibrate() + : null; + widget.selectedCollections.add(collection); + } + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(); + } + setState(() {}); + } + GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) { return GestureDetector( onTap: () async { @@ -139,8 +173,8 @@ class AlbumVerticalListWidget extends StatelessWidget { bool hasVerifiedLock = false; late final Collection? collection; - if (actionType == CollectionActionType.moveToHiddenCollection || - actionType == CollectionActionType.addToHiddenAlbum) { + if (widget.actionType == CollectionActionType.moveToHiddenCollection || + widget.actionType == CollectionActionType.addToHiddenAlbum) { collection = await CollectionsService.instance.createHiddenAlbum(albumName); hasVerifiedLock = true; @@ -154,7 +188,7 @@ class AlbumVerticalListWidget extends StatelessWidget { collection, showProgressDialog: false, )) { - if (actionType == CollectionActionType.restoreFiles) { + if (widget.actionType == CollectionActionType.restoreFiles) { showShortToast( context, 'Restored files to album ' + albumName, @@ -199,19 +233,20 @@ class AlbumVerticalListWidget extends StatelessWidget { bool shouldNavigateToCollection = false; bool hasVerifiedLock = false; - if (actionType == CollectionActionType.addFiles) { + if (widget.actionType == CollectionActionType.addFiles) { toastMessage = S.of(context).addedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; - } else if (actionType == CollectionActionType.moveFiles || - actionType == CollectionActionType.restoreFiles || - actionType == CollectionActionType.unHide) { + } else if (widget.actionType == CollectionActionType.moveFiles || + widget.actionType == CollectionActionType.restoreFiles || + widget.actionType == CollectionActionType.unHide) { toastMessage = S.of(context).movedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; - } else if (actionType == CollectionActionType.moveToHiddenCollection) { + } else if (widget.actionType == + CollectionActionType.moveToHiddenCollection) { toastMessage = S.of(context).movedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; hasVerifiedLock = true; - } else if (actionType == CollectionActionType.addToHiddenAlbum) { + } else if (widget.actionType == CollectionActionType.addToHiddenAlbum) { toastMessage = S.of(context).addedSuccessfullyTo(item.displayName); shouldNavigateToCollection = true; hasVerifiedLock = true; @@ -226,7 +261,6 @@ class AlbumVerticalListWidget extends StatelessWidget { } if (shouldNavigateToCollection) { Navigator.pop(context); - await _navigateToCollection( context, item, @@ -241,7 +275,7 @@ class AlbumVerticalListWidget extends StatelessWidget { Collection collection, { bool showProgressDialog = true, }) async { - switch (actionType) { + switch (widget.actionType) { case CollectionActionType.addFiles: return _addToCollection( context, @@ -256,8 +290,6 @@ class AlbumVerticalListWidget extends StatelessWidget { return _restoreFilesToCollection(context, collection.id); case CollectionActionType.shareCollection: return _showShareCollectionPage(context, collection); - case CollectionActionType.collectPhotos: - return _createCollaborativeLink(context, collection); case CollectionActionType.moveToHiddenCollection: return _moveFilesToCollection(context, collection.id); case CollectionActionType.addToHiddenAlbum: @@ -279,76 +311,6 @@ class AlbumVerticalListWidget extends StatelessWidget { ); } - Future _createCollaborativeLink( - BuildContext context, - Collection collection, - ) async { - final CollectionActions collectionActions = - CollectionActions(CollectionsService.instance); - - if (collection.hasLink) { - if (collection.publicURLs.first.enableCollect) { - if (Configuration.instance.getUserID() == collection.owner.id) { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - } - showShortToast( - context, - S.of(context).thisAlbumAlreadyHDACollaborativeLink, - ); - return Future.value(false); - } else { - try { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - // ignore: unawaited_futures - CollectionsService.instance - .updateShareUrl(collection, {'enableCollect': true}).then( - (value) => showShortToast( - context, - S.of(context).collaborativeLinkCreatedFor(collection.displayName), - ), - ); - return true; - } catch (e) { - await showGenericErrorDialog(context: context, error: e); - return false; - } - } - } - final bool result = await collectionActions.enableUrl( - context, - collection, - enableCollect: true, - ); - if (result) { - showShortToast( - context, - S.of(context).collaborativeLinkCreatedFor(collection.displayName), - ); - if (Configuration.instance.getUserID() == collection.owner.id) { - unawaited( - routeToPage( - context, - ShareCollectionPage(collection), - ), - ); - } else { - await showGenericErrorDialog(context: context, error: result); - _logger.severe("Cannot share collections owned by others"); - } - } - return result; - } - Future _showShareCollectionPage( BuildContext context, Collection collection, @@ -379,11 +341,11 @@ class AlbumVerticalListWidget extends StatelessWidget { context, collectionID, showProgressDialog, - selectedFiles: selectedFiles?.files.toList(), - sharedFiles: sharedFiles, + selectedFiles: widget.selectedFiles?.files.toList(), + sharedFiles: widget.sharedFiles, ); if (result) { - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); } return result; } @@ -393,8 +355,8 @@ class AlbumVerticalListWidget extends StatelessWidget { int toCollectionID, ) async { late final String message; - if (actionType == CollectionActionType.moveFiles || - actionType == CollectionActionType.moveToHiddenCollection) { + if (widget.actionType == CollectionActionType.moveFiles || + widget.actionType == CollectionActionType.moveToHiddenCollection) { message = S.of(context).movingFilesToAlbum; } else { message = S.of(context).unhidingFilesToAlbum; @@ -403,15 +365,16 @@ class AlbumVerticalListWidget extends StatelessWidget { final dialog = createProgressDialog(context, message, isDismissible: true); await dialog.show(); try { - final int fromCollectionID = selectedFiles!.files.first.collectionID!; + final int fromCollectionID = + widget.selectedFiles!.files.first.collectionID!; await CollectionsService.instance.move( - selectedFiles!.files.toList(), + widget.selectedFiles!.files.toList(), toCollectionID: toCollectionID, fromCollectionID: fromCollectionID, ); await dialog.hide(); unawaited(RemoteSyncService.instance.sync(silently: true)); - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); return true; } on AssertionError catch (e) { @@ -439,9 +402,9 @@ class AlbumVerticalListWidget extends StatelessWidget { await dialog.show(); try { await CollectionsService.instance - .restore(toCollectionID, selectedFiles!.files.toList()); + .restore(toCollectionID, widget.selectedFiles!.files.toList()); unawaited(RemoteSyncService.instance.sync(silently: true)); - selectedFiles?.clearAll(); + widget.selectedFiles?.clearAll(); await dialog.hide(); return true; } on AssertionError catch (e) { diff --git a/mobile/lib/ui/collections/button/archived_button.dart b/mobile/lib/ui/collections/button/archived_button.dart index 6676d4dcc5..0b0e674334 100644 --- a/mobile/lib/ui/collections/button/archived_button.dart +++ b/mobile/lib/ui/collections/button/archived_button.dart @@ -28,7 +28,7 @@ class ArchivedCollectionsButton extends StatelessWidget { padding: const EdgeInsets.all(0), side: BorderSide( width: 0.5, - color: Theme.of(context).iconTheme.color!.withOpacity(0.24), + color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24), ), ), child: SizedBox( diff --git a/mobile/lib/ui/collections/button/hidden_button.dart b/mobile/lib/ui/collections/button/hidden_button.dart index 8172d3c6a2..77358e79e8 100644 --- a/mobile/lib/ui/collections/button/hidden_button.dart +++ b/mobile/lib/ui/collections/button/hidden_button.dart @@ -23,7 +23,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget { padding: const EdgeInsets.all(0), side: BorderSide( width: 0.5, - color: Theme.of(context).iconTheme.color!.withOpacity(0.24), + color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24), ), ), child: SizedBox( diff --git a/mobile/lib/ui/collections/button/trash_button.dart b/mobile/lib/ui/collections/button/trash_button.dart index b6e71006fc..96fec3f54c 100644 --- a/mobile/lib/ui/collections/button/trash_button.dart +++ b/mobile/lib/ui/collections/button/trash_button.dart @@ -52,7 +52,7 @@ class _TrashSectionButtonState extends State { padding: const EdgeInsets.all(0), side: BorderSide( width: 0.5, - color: Theme.of(context).iconTheme.color!.withOpacity(0.24), + color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24), ), ), child: SizedBox( diff --git a/mobile/lib/ui/collections/button/uncategorized_button.dart b/mobile/lib/ui/collections/button/uncategorized_button.dart index 29800df76e..447be85eee 100644 --- a/mobile/lib/ui/collections/button/uncategorized_button.dart +++ b/mobile/lib/ui/collections/button/uncategorized_button.dart @@ -33,7 +33,7 @@ class UnCategorizedCollections extends StatelessWidget { padding: const EdgeInsets.all(0), side: BorderSide( width: 0.5, - color: Theme.of(context).iconTheme.color!.withOpacity(0.24), + color: Theme.of(context).iconTheme.color!.withValues(alpha: 0.24), ), ), child: SizedBox( diff --git a/mobile/lib/ui/collections/collection_action_sheet.dart b/mobile/lib/ui/collections/collection_action_sheet.dart index 370982c8a6..a44d2246c5 100644 --- a/mobile/lib/ui/collections/collection_action_sheet.dart +++ b/mobile/lib/ui/collections/collection_action_sheet.dart @@ -11,6 +11,8 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/actions/collection/collection_file_actions.dart"; +import "package:photos/ui/actions/collection/collection_sharing_actions.dart"; import 'package:photos/ui/collections/album/vertical_list.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/bottom_of_title_bar_widget.dart'; @@ -18,6 +20,8 @@ import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/components/text_input_widget.dart"; import 'package:photos/ui/components/title_bar_title_widget.dart'; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/utils/separators_util.dart"; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; enum CollectionActionType { @@ -26,7 +30,6 @@ enum CollectionActionType { restoreFiles, unHide, shareCollection, - collectPhotos, addToHiddenAlbum, moveToHiddenCollection, } @@ -53,10 +56,9 @@ String _actionName( case CollectionActionType.shareCollection: text = S.of(context).share; break; - case CollectionActionType.collectPhotos: - text = S.of(context).share; case CollectionActionType.addToHiddenAlbum: text = S.of(context).addToHiddenAlbum; + break; case CollectionActionType.moveToHiddenCollection: text = S.of(context).moveToHiddenAlbum; break; @@ -90,7 +92,7 @@ void showCollectionActionSheet( topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, - enableDrag: false, + enableDrag: true, ); } @@ -113,8 +115,10 @@ class CollectionActionSheet extends StatefulWidget { class _CollectionActionSheetState extends State { late final bool _showOnlyHiddenCollections; - static const int cancelButtonSize = 80; + late final bool _enableSelection; + static const int okButtonSize = 80; String _searchQuery = ""; + final _selectedCollections = []; @override void initState() { @@ -122,6 +126,9 @@ class _CollectionActionSheetState extends State { _showOnlyHiddenCollections = widget.actionType == CollectionActionType.moveToHiddenCollection || widget.actionType == CollectionActionType.addToHiddenAlbum; + _enableSelection = (widget.actionType == CollectionActionType.addFiles || + widget.actionType == CollectionActionType.addToHiddenAlbum) && + (widget.sharedFiles == null || widget.sharedFiles!.isEmpty); } @override @@ -129,11 +136,13 @@ class _CollectionActionSheetState extends State { final filesCount = widget.sharedFiles != null ? widget.sharedFiles!.length : widget.selectedFiles?.files.length ?? 0; - final bottomInset = MediaQuery.of(context).viewInsets.bottom; + final bottomInset = MediaQuery.viewInsetsOf(context).bottom; final isKeyboardUp = bottomInset > 100; + final double bottomPadding = + max(0, bottomInset - (_enableSelection ? okButtonSize : 0)); return Padding( padding: EdgeInsets.only( - bottom: isKeyboardUp ? bottomInset - cancelButtonSize : 0, + bottom: isKeyboardUp ? bottomPadding : 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -161,6 +170,7 @@ class _CollectionActionSheetState extends State { caption: widget.showOptionToCreateNewAlbum ? S.of(context).createOrSelectAlbum : S.of(context).selectAlbum, + showCloseButton: true, ), Padding( padding: const EdgeInsets.only( @@ -173,12 +183,11 @@ class _CollectionActionSheetState extends State { prefixIcon: Icons.search_rounded, onChange: (value) { setState(() { - _searchQuery = value; + _searchQuery = value.trim(); }); }, isClearable: true, shouldUnfocusOnClearOrSubmit: true, - borderRadius: 2, ), ), _getCollectionItems(), @@ -192,15 +201,16 @@ class _CollectionActionSheetState extends State { decoration: BoxDecoration( border: Border( top: BorderSide( - color: getEnteColorScheme(context).strokeFaint, + color: _enableSelection + ? getEnteColorScheme(context).strokeFaint + : Colors.transparent, ), ), ), - child: ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - isInAlert: true, - labelText: S.of(context).cancel, + child: Column( + children: [ + ..._actionButtons(), + ], ), ), ), @@ -213,6 +223,46 @@ class _CollectionActionSheetState extends State { ); } + List _actionButtons() { + final List widgets = []; + if (_enableSelection) { + widgets.add( + ButtonWidget( + key: const ValueKey('add_button'), + buttonType: ButtonType.primary, + isInAlert: true, + labelText: S.of(context).add, + shouldSurfaceExecutionStates: false, + isDisabled: _selectedCollections.isEmpty, + onTap: () async { + final CollectionActions collectionActions = + CollectionActions(CollectionsService.instance); + final result = await collectionActions.addToMultipleCollections( + context, + _selectedCollections, + true, + selectedFiles: widget.selectedFiles?.files.toList(), + ); + if (result) { + showShortToast( + context, + "Added successfully to " + + _selectedCollections.length.toString() + + " albums", + ); + widget.selectedFiles?.clearAll(); + } + }, + ), + ); + } + final widgetsWithSpaceBetween = addSeparators( + widgets, + const SizedBox(height: 8), + ); + return widgetsWithSpaceBetween; + } + Flexible _getCollectionItems() { return Flexible( child: Padding( @@ -239,6 +289,7 @@ class _CollectionActionSheetState extends State { : collections; return Scrollbar( thumbVisibility: true, + interactive: true, radius: const Radius.circular(2), child: Padding( padding: const EdgeInsets.only(right: 12), @@ -249,6 +300,11 @@ class _CollectionActionSheetState extends State { widget.sharedFiles, _searchQuery, shouldShowCreateAlbum, + enableSelection: _enableSelection, + selectedCollections: _selectedCollections, + onSelectionChanged: () { + setState(() {}); + }, ), ), ); @@ -273,14 +329,12 @@ class _CollectionActionSheetState extends State { }); return hiddenCollections; } else { - final bool includeUncategorized = - widget.actionType == CollectionActionType.restoreFiles; final List collections = CollectionsService.instance.getCollectionsForUI( // in collections where user is a collaborator, only addTo and remove // action can to be performed includeCollab: widget.actionType == CollectionActionType.addFiles, - includeUncategorized: includeUncategorized, + includeUncategorized: true, ); collections.sort((first, second) { return compareAsciiLowerCaseNatural( @@ -296,8 +350,7 @@ class _CollectionActionSheetState extends State { if (collection.isQuickLinkCollection() || collection.type == CollectionType.favorites || collection.type == CollectionType.uncategorized) { - if (collection.type == CollectionType.uncategorized && - includeUncategorized) { + if (collection.type == CollectionType.uncategorized) { uncategorized = collection; } continue; @@ -308,13 +361,12 @@ class _CollectionActionSheetState extends State { unpinned.add(collection); } } - return pinned + unpinned + (uncategorized != null ? [uncategorized] : []); + return (uncategorized != null ? [uncategorized] + pinned + unpinned : []); } } void _removeIncomingCollections(List items) { - if (widget.actionType == CollectionActionType.shareCollection || - widget.actionType == CollectionActionType.collectPhotos) { + if (widget.actionType == CollectionActionType.shareCollection) { final ownerID = Configuration.instance.getUserID(); items.removeWhere( (e) => !e.isOwner(ownerID!), diff --git a/mobile/lib/ui/collections/collection_list_page.dart b/mobile/lib/ui/collections/collection_list_page.dart index 974acdfdbb..3bc168a9d6 100644 --- a/mobile/lib/ui/collections/collection_list_page.dart +++ b/mobile/lib/ui/collections/collection_list_page.dart @@ -2,11 +2,20 @@ import "dart:async"; import 'package:flutter/material.dart'; import "package:photos/core/event_bus.dart"; +import "package:photos/events/album_sort_order_change_event.dart"; import "package:photos/events/collection_updated_event.dart"; +import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; +import "package:photos/models/selected_albums.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collections/flex_grid_view.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; +import "package:photos/ui/components/searchable_appbar.dart"; +import "package:photos/ui/viewer/actions/album_selection_overlay_bar.dart"; +import "package:photos/utils/local_settings.dart"; enum UISectionType { incomingCollections, @@ -38,6 +47,12 @@ class _CollectionListPageState extends State { late StreamSubscription _collectionUpdatesSubscription; List? collections; + AlbumSortKey? sortKey; + AlbumViewType? albumViewType; + AlbumSortDirection? albumSortDirection; + String _searchQuery = ""; + final _selectedAlbum = SelectedAlbums(); + late final ScrollController _scrollController; @override void initState() { @@ -47,36 +62,77 @@ class _CollectionListPageState extends State { Bus.instance.on().listen((event) async { unawaited(refreshCollections()); }); + sortKey = localSettings.albumSortKey(); + albumViewType = localSettings.albumViewType(); + albumSortDirection = localSettings.albumSortDirection(); + _scrollController = ScrollController( + initialScrollOffset: widget.initialScrollOffset ?? 0, + ); } @override void dispose() { _collectionUpdatesSubscription.cancel(); + _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final displayLimitCount = (collections?.length ?? 0) + + (widget.tag.isEmpty && _searchQuery.isEmpty ? 1 : 0); + final bool enableSelectionMode = + widget.sectionType == UISectionType.homeCollections || + widget.sectionType == UISectionType.outgoingCollections || + widget.sectionType == UISectionType.incomingCollections; return Scaffold( body: SafeArea( - child: CustomScrollView( - physics: const BouncingScrollPhysics(), - controller: ScrollController( - initialScrollOffset: widget.initialScrollOffset ?? 0, - ), - slivers: [ - SliverAppBar( - elevation: 0, - title: Hero( - tag: widget.tag, - child: widget.appTitle ?? const SizedBox.shrink(), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Scrollbar( + interactive: true, + controller: _scrollController, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + controller: _scrollController, + slivers: [ + SearchableAppBar( + title: widget.appTitle ?? const SizedBox.shrink(), + heroTag: widget.tag, + onSearch: (value) { + setState(() { + _searchQuery = value; + }); + refreshCollections(); + }, + onSearchClosed: () { + setState(() { + _searchQuery = ''; + }); + refreshCollections(); + }, + actions: [ + _sortMenu(collections!), + ], + ), + CollectionsFlexiGridViewWidget( + collections, + displayLimitCount: displayLimitCount, + tag: widget.tag, + enableSelectionMode: enableSelectionMode, + albumViewType: albumViewType ?? AlbumViewType.grid, + selectedAlbums: _selectedAlbum, + scrollBottomSafeArea: 140, + ), + ], ), - floating: true, ), - CollectionsFlexiGridViewWidget( - collections, - displayLimitCount: collections?.length ?? 0, - tag: widget.tag, + AlbumSelectionOverlayBar( + _selectedAlbum, + widget.sectionType, + collections!, + showSelectAllButton: true, ), ], ), @@ -84,19 +140,133 @@ class _CollectionListPageState extends State { ); } + Widget _sortMenu(List collections) { + final colorTheme = getEnteColorScheme(context); + + Widget sortOptionText(AlbumSortKey key) { + String text = key.toString(); + switch (key) { + case AlbumSortKey.albumName: + text = S.of(context).name; + break; + case AlbumSortKey.newestPhoto: + text = S.of(context).newest; + break; + case AlbumSortKey.lastUpdated: + text = S.of(context).lastUpdated; + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + text, + ), + Icon( + sortKey == key + ? (albumSortDirection == AlbumSortDirection.ascending + ? Icons.arrow_upward + : Icons.arrow_downward) + : null, + size: 18, + ), + ], + ); + } + + return Theme( + data: Theme.of(context).copyWith( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + ), + child: Row( + children: [ + IconButtonWidget( + icon: albumViewType == AlbumViewType.grid + ? Icons.view_list_outlined + : Icons.grid_view_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorTheme.blurStrokePressed, + onTap: () async { + setState(() { + albumViewType = albumViewType == AlbumViewType.grid + ? AlbumViewType.list + : AlbumViewType.grid; + }); + await localSettings.setAlbumViewType(albumViewType!); + }, + ), + GestureDetector( + onTapDown: (TapDownDetails details) async { + final int? selectedValue = await showMenu( + color: colorTheme.backgroundElevated, + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy + 50, + ), + items: List.generate(AlbumSortKey.values.length, (index) { + return PopupMenuItem( + value: index, + child: sortOptionText(AlbumSortKey.values[index]), + ); + }), + ); + if (selectedValue != null) { + sortKey = AlbumSortKey.values[selectedValue]; + await localSettings.setAlbumSortKey(sortKey!); + albumSortDirection = + albumSortDirection == AlbumSortDirection.ascending + ? AlbumSortDirection.descending + : AlbumSortDirection.ascending; + await localSettings.setAlbumSortDirection(albumSortDirection!); + await refreshCollections(); + Bus.instance.fire(AlbumSortOrderChangeEvent()); + } + }, + child: IconButtonWidget( + icon: Icons.sort_outlined, + iconButtonType: IconButtonType.secondary, + iconColor: colorTheme.blurStrokePressed, + ), + ), + ], + ), + ); + } + Future refreshCollections() async { if (widget.sectionType == UISectionType.incomingCollections || widget.sectionType == UISectionType.outgoingCollections) { final SharedCollections sharedCollections = - CollectionsService.instance.getSharedCollections(); + await CollectionsService.instance.getSharedCollections(); if (widget.sectionType == UISectionType.incomingCollections) { collections = sharedCollections.incoming; } else { collections = sharedCollections.outgoing; } + if (_searchQuery.isNotEmpty) { + collections = widget.collections + ?.where( + (c) => c.displayName + .toLowerCase() + .contains(_searchQuery.toLowerCase()), + ) + .toList(); + } } else if (widget.sectionType == UISectionType.homeCollections) { collections = await CollectionsService.instance.getCollectionForOnEnteSection(); + if (_searchQuery.isNotEmpty) { + collections = widget.collections + ?.where( + (c) => c.displayName + .toLowerCase() + .contains(_searchQuery.toLowerCase()), + ) + .toList(); + } } if (mounted) { setState(() {}); diff --git a/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart b/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart index d42bad8dd5..0e5a19edf6 100644 --- a/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart +++ b/mobile/lib/ui/collections/device/device_folders_vertical_grid_view.dart @@ -66,8 +66,8 @@ class _DeviceFolderVerticalGridViewBodyState Width changes dynamically with screen width such that we can fit 2 in one row. Keep the width integral (center the albums to distribute excess pixels) */ - static const maxThumbnailWidth = 224.0; - static const fixedGapBetweenAlbum = 8.0; + static const maxThumbnailWidth = 170.0; + static const fixedGapBetweenAlbum = 2.0; static const minGapForHorizontalPadding = 8.0; @override @@ -103,7 +103,7 @@ class _DeviceFolderVerticalGridViewBodyState if (snapshot.hasData) { final double screenWidth = MediaQuery.of(context).size.width; final int albumsCountInOneRow = - max(screenWidth ~/ maxThumbnailWidth, 2); + max(screenWidth ~/ maxThumbnailWidth, 3); final double gapBetweenAlbums = (albumsCountInOneRow - 1) * fixedGapBetweenAlbum; final double gapOnSizeOfAlbums = minGapForHorizontalPadding + diff --git a/mobile/lib/ui/collections/flex_grid_view.dart b/mobile/lib/ui/collections/flex_grid_view.dart index 4114ff0493..34e2402dfc 100644 --- a/mobile/lib/ui/collections/flex_grid_view.dart +++ b/mobile/lib/ui/collections/flex_grid_view.dart @@ -1,20 +1,34 @@ +import "dart:async"; import 'dart:math'; import 'package:flutter/material.dart'; +import "package:flutter/services.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/clear_album_selections_event.dart"; +import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; +import "package:photos/models/collection/collection_items.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/ui/collections/album/list_item.dart"; +import "package:photos/ui/collections/album/new_list_item.dart"; +import "package:photos/ui/collections/album/new_row_item.dart"; import "package:photos/ui/collections/album/row_item.dart"; +import "package:photos/ui/viewer/gallery/collection_page.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/navigation_util.dart"; -class CollectionsFlexiGridViewWidget extends StatelessWidget { +class CollectionsFlexiGridViewWidget extends StatefulWidget { /* - Aspect ratio 1:1 Max width 224 Fixed gap 8 - Width changes dynamically with screen width such that we can fit 2 in one row. - Keep the width integral (center the albums to distribute excess pixels) - */ - static const maxThumbnailWidth = 224.0; - static const fixedGapBetweenAlbum = 8.0; - static const minGapForHorizontalPadding = 8.0; - static const collectionItemsToPreload = 20; + Aspect ratio 1:1 + Width changes dynamically with screen width + */ + static const maxThumbnailWidth = 224.0; + static const crossAxisSpacing = 8.0; + static const horizontalPadding = 16.0; final List? collections; // At max how many albums to display final int displayLimitCount; @@ -23,49 +37,271 @@ class CollectionsFlexiGridViewWidget extends StatelessWidget { final bool shrinkWrap; final String tag; + final AlbumViewType albumViewType; + final bool enableSelectionMode; + final bool shouldShowCreateAlbum; + final SelectedAlbums? selectedAlbums; + final double scrollBottomSafeArea; + final bool onlyAllowSelection; + const CollectionsFlexiGridViewWidget( this.collections, { - this.displayLimitCount = 10, + this.displayLimitCount = 9, this.shrinkWrap = false, this.tag = "", + this.enableSelectionMode = false, super.key, + this.albumViewType = AlbumViewType.grid, + this.shouldShowCreateAlbum = false, + this.selectedAlbums, + this.scrollBottomSafeArea = 8, + this.onlyAllowSelection = false, }); @override - Widget build(BuildContext context) { - final double screenWidth = MediaQuery.of(context).size.width; - final int albumsCountInOneRow = max(screenWidth ~/ maxThumbnailWidth, 2); - final double gapBetweenAlbums = - (albumsCountInOneRow - 1) * fixedGapBetweenAlbum; - // gapOnSizeOfAlbums will be - final double gapOnSizeOfAlbums = minGapForHorizontalPadding + - (screenWidth - gapBetweenAlbums - (2 * minGapForHorizontalPadding)) % - albumsCountInOneRow; + State createState() => + _CollectionsFlexiGridViewWidgetState(); +} - final double sideOfThumbnail = - (screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) / - albumsCountInOneRow; +class _CollectionsFlexiGridViewWidgetState + extends State { + bool isAnyAlbumSelected = false; + late StreamSubscription + _clearAlbumSelectionSubscription; + + @override + void initState() { + _clearAlbumSelectionSubscription = + Bus.instance.on().listen((event) { + if (mounted) { + setState(() { + isAnyAlbumSelected = false; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _clearAlbumSelectionSubscription.cancel(); + super.dispose(); + } + + Future _toggleAlbumSelection(Collection c) async { + await HapticFeedback.lightImpact(); + widget.selectedAlbums!.toggleSelection(c); + setState(() { + isAnyAlbumSelected = widget.selectedAlbums!.albums.isNotEmpty; + }); + } + + Future _navigateToCollectionPage(Collection c) async { + final thumbnail = await CollectionsService.instance.getCover(c); + // ignore: unawaited_futures + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(c, thumbnail), + ), + ); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !isAnyAlbumSelected, + onPopInvokedWithResult: (didPop, _) { + if (didPop) { + return; + } + if (isAnyAlbumSelected) { + widget.selectedAlbums!.clearAll(); + setState(() { + isAnyAlbumSelected = false; + }); + } + }, + child: widget.albumViewType == AlbumViewType.grid + ? _buildGridView(context, const ValueKey("grid_view")) + : _buildListView(context, const ValueKey("list_view")), + ); + } + + Widget _buildGridView(BuildContext context, Key key) { + final double screenWidth = MediaQuery.sizeOf(context).width; + final int albumsCountInCrossAxis = + max(screenWidth ~/ CollectionsFlexiGridViewWidget.maxThumbnailWidth, 3); + final double totalCrossAxisSpacing = (albumsCountInCrossAxis - 1) * + CollectionsFlexiGridViewWidget.crossAxisSpacing; + + final double sideOfThumbnail = (screenWidth - + totalCrossAxisSpacing - + CollectionsFlexiGridViewWidget.horizontalPadding) / + albumsCountInCrossAxis; + + final int totalCollections = widget.collections!.length; + final bool showCreateAlbum = widget.shouldShowCreateAlbum; + final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0); + final int displayItemCount = min(totalItemCount, widget.displayLimitCount); return SliverPadding( - padding: const EdgeInsets.all(8), + key: key, + padding: EdgeInsets.only( + top: 8, + left: CollectionsFlexiGridViewWidget.horizontalPadding / 2, + right: CollectionsFlexiGridViewWidget.horizontalPadding / 2, + bottom: widget.scrollBottomSafeArea, + ), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { + if (showCreateAlbum && index == 0) { + return NewAlbumRowItemWidget( + height: sideOfThumbnail, + width: sideOfThumbnail, + ); + } + final collectionIndex = showCreateAlbum ? index - 1 : index; return AlbumRowItemWidget( - collections![index], + widget.collections![collectionIndex], sideOfThumbnail, - tag: tag, + tag: widget.tag, + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) { + isAnyAlbumSelected || widget.onlyAllowSelection + ? _toggleAlbumSelection(c) + : _navigateToCollectionPage(c); + }, + onLongPressCallback: widget.enableSelectionMode + ? (c) { + isAnyAlbumSelected || widget.onlyAllowSelection + ? _navigateToCollectionPage(c) + : _toggleAlbumSelection(c); + } + : null, ); }, - childCount: min(collections!.length, displayLimitCount), + childCount: displayItemCount, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: albumsCountInOneRow, - mainAxisSpacing: 4, - crossAxisSpacing: gapBetweenAlbums, + crossAxisCount: albumsCountInCrossAxis, + mainAxisSpacing: 2, + crossAxisSpacing: CollectionsFlexiGridViewWidget.crossAxisSpacing, childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 46), ), ), ); } + + Widget _buildListView(BuildContext context, Key key) { + final int totalCollections = widget.collections?.length ?? 0; + final bool showCreateAlbum = + widget.shouldShowCreateAlbum && !isAnyAlbumSelected; + final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0); + final int displayItemCount = min(totalItemCount, widget.displayLimitCount); + + return SliverPadding( + key: key, + padding: EdgeInsets.only( + top: 8, + left: 8, + right: 8, + bottom: widget.scrollBottomSafeArea, + ), + sliver: SliverPrototypeExtentList( + prototypeItem: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: showCreateAlbum + ? const NewAlbumListItemWidget() + : AlbumListItemWidget( + widget.collections![0], + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) {}, + ), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + Widget item; + + if (showCreateAlbum && index == 0) { + item = GestureDetector( + onTap: () async { + GestureDetector( + onTap: () async { + final result = await showTextInputDialog( + context, + title: S.of(context).newAlbum, + submitButtonLabel: S.of(context).create, + hintText: S.of(context).enterAlbumName, + alwaysShowSuccessState: false, + initialValue: "", + textCapitalization: TextCapitalization.words, + popnavAfterSubmission: false, + onSubmit: (String text) async { + if (text.trim() == "") { + return; + } + + try { + final Collection c = await CollectionsService + .instance + .createAlbum(text); + // ignore: unawaited_futures + await routeToPage( + context, + CollectionPage(CollectionWithThumbnail(c, null)), + ); + Navigator.of(context).pop(); + } catch (e, s) { + Logger("CreateNewAlbumIcon") + .severe("Failed to rename album", e, s); + rethrow; + } + }, + ); + + if (result is Exception) { + await showGenericErrorDialog( + context: context, + error: result, + ); + } + }, + child: const NewAlbumListItemWidget(), + ); + }, + child: const NewAlbumListItemWidget(), + ); + } else { + final collectionIndex = showCreateAlbum ? index - 1 : index; + + item = AlbumListItemWidget( + widget.collections![collectionIndex], + selectedAlbums: widget.selectedAlbums, + onTapCallback: (c) { + isAnyAlbumSelected + ? _toggleAlbumSelection(c) + : _navigateToCollectionPage(c); + }, + onLongPressCallback: widget.enableSelectionMode + ? (c) { + isAnyAlbumSelected + ? _navigateToCollectionPage(c) + : _toggleAlbumSelection(c); + } + : null, + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: item, + ); + }, + childCount: displayItemCount, + ), + ), + ); + } } diff --git a/mobile/lib/ui/common/date_input.dart b/mobile/lib/ui/common/date_input.dart index 41fbccb76a..f9624d5167 100644 --- a/mobile/lib/ui/common/date_input.dart +++ b/mobile/lib/ui/common/date_input.dart @@ -121,8 +121,10 @@ class _DatePickerFieldState extends State { } Future _showDatePicker() async { + final Locale locale = await getFormatLocale(); final DateTime? picked = await showDatePicker( context: context, + locale: locale, initialDate: _selectedDate ?? DateTime.now(), firstDate: widget.firstDate ?? DateTime(1900), lastDate: widget.lastDate ?? DateTime(2100), diff --git a/mobile/lib/ui/common/loading_widget.dart b/mobile/lib/ui/common/loading_widget.dart index ffdc438901..fc10eeb18a 100644 --- a/mobile/lib/ui/common/loading_widget.dart +++ b/mobile/lib/ui/common/loading_widget.dart @@ -22,10 +22,12 @@ class EnteLoadingWidget extends StatelessWidget { padding: EdgeInsets.all(padding), child: SizedBox.fromSize( size: Size.square(size), - child: CircularProgressIndicator( - strokeWidth: 2, - color: color ?? getEnteColorScheme(context).strokeBase, - strokeCap: StrokeCap.round, + child: RepaintBoundary( + child: CircularProgressIndicator( + strokeWidth: 2, + color: color ?? getEnteColorScheme(context).strokeBase, + strokeCap: StrokeCap.round, + ), ), ), ), diff --git a/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart b/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart new file mode 100644 index 0000000000..db4dee8d9a --- /dev/null +++ b/mobile/lib/ui/components/bottom_action_bar/album_action_bar_widget.dart @@ -0,0 +1,86 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/theme/ente_theme.dart"; + +class AlbumActionBarWidget extends StatefulWidget { + final SelectedAlbums? selectedAlbums; + final VoidCallback? onCancel; + const AlbumActionBarWidget({ + super.key, + this.selectedAlbums, + this.onCancel, + }); + + @override + State createState() => _AlbumActionBarWidgetState(); +} + +class _AlbumActionBarWidgetState extends State { + final ValueNotifier _selectedAlbumNotifier = ValueNotifier(0); + + @override + void initState() { + widget.selectedAlbums?.addListener(_selectedAlbumListener); + super.initState(); + } + + @override + void dispose() { + widget.selectedAlbums?.removeListener(_selectedAlbumListener); + _selectedAlbumNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + return SizedBox( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: ValueListenableBuilder( + valueListenable: _selectedAlbumNotifier, + builder: (context, value, child) { + return Text( + S.of(context).selectedAlbums( + widget.selectedAlbums?.albums.length ?? 0, + ), + style: textTheme.miniMuted, + ); + }, + ), + ), + Flexible( + flex: 1, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onCancel?.call(); + }, + child: Align( + alignment: Alignment.centerRight, + child: Text( + S.of(context).cancel, + style: textTheme.mini, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _selectedAlbumListener() { + _selectedAlbumNotifier.value = widget.selectedAlbums?.albums.length ?? 0; + } +} diff --git a/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart b/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart new file mode 100644 index 0000000000..3312584085 --- /dev/null +++ b/mobile/lib/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart @@ -0,0 +1,59 @@ +import "package:flutter/material.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/collections/collection_list_page.dart"; +import "package:photos/ui/components/bottom_action_bar/album_action_bar_widget.dart"; +import "package:photos/ui/components/divider_widget.dart"; +import "package:photos/ui/viewer/actions/album_selection_action_widget.dart"; + +class AlbumBottomActionBarWidget extends StatelessWidget { + final SelectedAlbums selectedAlbums; + final VoidCallback? onCancel; + final Color? backgroundColor; + final UISectionType sectionType; + + const AlbumBottomActionBarWidget( + this.selectedAlbums, + this.sectionType, { + super.key, + this.backgroundColor, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + final bottomPadding = MediaQuery.paddingOf(context).bottom; + final widthOfScreen = MediaQuery.sizeOf(context).width; + final colorScheme = getEnteColorScheme(context); + final double leftRightPadding = widthOfScreen > restrictedMaxWidth + ? (widthOfScreen - restrictedMaxWidth) / 2 + : 0; + return Container( + decoration: BoxDecoration( + color: backgroundColor ?? colorScheme.backgroundElevated2, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.only( + top: 4, + bottom: bottomPadding, + right: leftRightPadding, + left: leftRightPadding, + ), + child: Column( + children: [ + const SizedBox(height: 8), + AlbumSelectionActionWidget(selectedAlbums, sectionType), + const DividerWidget(dividerType: DividerType.bottomBar), + AlbumActionBarWidget( + selectedAlbums: selectedAlbums, + onCancel: onCancel, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/components/notification_widget.dart b/mobile/lib/ui/components/notification_widget.dart index 152f9044d3..88c8f98624 100644 --- a/mobile/lib/ui/components/notification_widget.dart +++ b/mobile/lib/ui/components/notification_widget.dart @@ -60,7 +60,7 @@ class NotificationWidget extends StatelessWidget { subTextStyle = textTheme.miniMuted; strokeColorScheme = colorScheme; boxShadow = [ - BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 1), + BoxShadow(color: Colors.black.withValues(alpha: 0.25), blurRadius: 1), ]; break; case NotificationType.goldenBanner: diff --git a/mobile/lib/ui/components/searchable_appbar.dart b/mobile/lib/ui/components/searchable_appbar.dart new file mode 100644 index 0000000000..4290713552 --- /dev/null +++ b/mobile/lib/ui/components/searchable_appbar.dart @@ -0,0 +1,132 @@ +import "package:flutter/material.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; + +class SearchableAppBar extends StatefulWidget { + final Widget title; + final List? actions; + final Function(String) onSearch; + final VoidCallback? onSearchClosed; + final String heroTag; + + const SearchableAppBar({ + super.key, + required this.title, + this.actions, + required this.onSearch, + this.onSearchClosed, + this.heroTag = "", + }); + + @override + State createState() => _SearchableAppBarState(); +} + +class _SearchableAppBarState extends State { + bool _isSearchActive = false; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + void _activateSearch() { + setState(() { + _isSearchActive = true; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + _searchFocusNode.requestFocus(); + }); + } + + void _deactivateSearch() { + setState(() { + _isSearchActive = false; + _searchController.clear(); + }); + _searchFocusNode.unfocus(); + if (widget.onSearchClosed != null) { + widget.onSearchClosed!(); + } + } + + @override + Widget build(BuildContext context) { + return SliverAppBar( + floating: true, + elevation: 0, + automaticallyImplyLeading: !_isSearchActive, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + child: _isSearchActive + ? _buildSearchField() + : Hero( + key: const ValueKey('titleBar'), + tag: widget.heroTag, + child: widget.title, + ), + ), + actions: _isSearchActive + ? null + : [ + IconButtonWidget( + icon: Icons.search, + iconButtonType: IconButtonType.secondary, + onTap: _activateSearch, + iconColor: getEnteColorScheme(context).blurStrokePressed, + ), + ...?widget.actions, + ], + ); + } + + Widget _buildSearchField() { + final colorScheme = getEnteColorScheme(context); + return Container( + key: const ValueKey('searchBar'), + alignment: Alignment.center, + child: TextFormField( + controller: _searchController, + focusNode: _searchFocusNode, + decoration: InputDecoration( + filled: true, + fillColor: colorScheme.fillFaint, + prefixIcon: Icon( + Icons.search_rounded, + color: colorScheme.strokeMuted, + ), + suffixIcon: IconButton( + icon: Icon( + Icons.cancel_rounded, + color: colorScheme.strokeMuted, + ), + onPressed: _deactivateSearch, + ), + border: const UnderlineInputBorder( + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.fromLTRB(12, 12, 0, 12), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.strokeFaint, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: widget.onSearch, + ), + ); + } +} diff --git a/mobile/lib/ui/components/title_bar_widget.dart b/mobile/lib/ui/components/title_bar_widget.dart index 9bdaf81c0f..2105d9b36b 100644 --- a/mobile/lib/ui/components/title_bar_widget.dart +++ b/mobile/lib/ui/components/title_bar_widget.dart @@ -14,6 +14,8 @@ class TitleBarWidget extends StatelessWidget { final bool isOnTopOfScreen; final Color? backgroundColor; final bool isSliver; + final double? expandedHeight; + const TitleBarWidget({ this.leading, this.title, @@ -26,6 +28,7 @@ class TitleBarWidget extends StatelessWidget { this.isOnTopOfScreen = true, this.backgroundColor, this.isSliver = true, + this.expandedHeight, super.key, }); @@ -40,7 +43,8 @@ class TitleBarWidget extends StatelessWidget { leadingWidth: 48, automaticallyImplyLeading: false, pinned: true, - expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102, + expandedHeight: + expandedHeight ?? (isFlexibleSpaceDisabled ? toolbarHeight : 102), centerTitle: false, titleSpacing: 4, title: TitleWidget( @@ -72,6 +76,7 @@ class TitleBarWidget extends StatelessWidget { flexibleSpaceTitle, flexibleSpaceCaption, toolbarHeight, + maxLines: expandedHeight == null ? 1 : 2, ), ); } else { @@ -112,6 +117,7 @@ class TitleBarWidget extends StatelessWidget { flexibleSpaceTitle, flexibleSpaceCaption, toolbarHeight, + maxLines: expandedHeight == null ? 1 : 2, ), ); } @@ -189,11 +195,14 @@ class FlexibleSpaceBarWidget extends StatelessWidget { final Widget? flexibleSpaceTitle; final String? flexibleSpaceCaption; final double toolbarHeight; + final int maxLines; + const FlexibleSpaceBarWidget( this.flexibleSpaceTitle, this.flexibleSpaceCaption, this.toolbarHeight, { super.key, + required this.maxLines, }); @override @@ -223,7 +232,7 @@ class FlexibleSpaceBarWidget extends StatelessWidget { flexibleSpaceCaption!, style: textTheme.smallMuted, overflow: TextOverflow.ellipsis, - maxLines: 1, + maxLines: maxLines, ), ], ), diff --git a/mobile/lib/ui/home/grant_permissions_widget.dart b/mobile/lib/ui/home/grant_permissions_widget.dart index 84aebd2b21..a9801cb38d 100644 --- a/mobile/lib/ui/home/grant_permissions_widget.dart +++ b/mobile/lib/ui/home/grant_permissions_widget.dart @@ -44,7 +44,7 @@ class _GrantPermissionsWidgetState extends State { isLightMode ? Image.asset( 'assets/loading_photos_background.png', - color: Colors.white.withOpacity(0.4), + color: Colors.white.withValues(alpha: 0.4), colorBlendMode: BlendMode.modulate, ) : Image.asset( diff --git a/mobile/lib/ui/home/home_bottom_nav_bar.dart b/mobile/lib/ui/home/home_bottom_nav_bar.dart index 67a0d98cf9..382c3e8210 100644 --- a/mobile/lib/ui/home/home_bottom_nav_bar.dart +++ b/mobile/lib/ui/home/home_bottom_nav_bar.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/tab_changed_event.dart'; +import "package:photos/models/selected_albums.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; @@ -10,12 +11,14 @@ import 'package:photos/ui/tabs/nav_bar.dart'; class HomeBottomNavigationBar extends StatefulWidget { const HomeBottomNavigationBar( - this.selectedFiles, { + this.selectedFiles, + this.selectedAlbums, { required this.selectedTabIndex, super.key, }); final SelectedFiles selectedFiles; + final SelectedAlbums selectedAlbums; final int selectedTabIndex; @override @@ -32,6 +35,7 @@ class _HomeBottomNavigationBarState extends State { super.initState(); currentTabIndex = widget.selectedTabIndex; widget.selectedFiles.addListener(_selectedFilesListener); + widget.selectedAlbums.addListener(_selectedAlbumsListener); _tabChangedEventSubscription = Bus.instance.on().listen((event) { if (event.source != TabChangedEventSource.tabBar) { @@ -56,6 +60,7 @@ class _HomeBottomNavigationBarState extends State { void dispose() { _tabChangedEventSubscription.cancel(); widget.selectedFiles.removeListener(_selectedFilesListener); + widget.selectedAlbums.removeListener(_selectedAlbumsListener); super.dispose(); } @@ -65,6 +70,12 @@ class _HomeBottomNavigationBarState extends State { } } + void _selectedAlbumsListener() { + if (mounted) { + setState(() {}); + } + } + void _onTabChange(int index, {String mode = 'tabChanged'}) { debugPrint("_TabChanged called via method $mode"); Bus.instance.fire( @@ -78,6 +89,7 @@ class _HomeBottomNavigationBarState extends State { @override Widget build(BuildContext context) { final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty; + final bool albumsAreSelected = widget.selectedAlbums.albums.isNotEmpty; final enteColorScheme = getEnteColorScheme(context); return SafeArea( @@ -85,9 +97,9 @@ class _HomeBottomNavigationBarState extends State { child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: filesAreSelected ? 0 : 56, + height: filesAreSelected || albumsAreSelected ? 0 : 56, child: IgnorePointer( - ignoring: filesAreSelected, + ignoring: filesAreSelected || albumsAreSelected, child: ListView( physics: const NeverScrollableScrollPhysics(), children: [ diff --git a/mobile/lib/ui/home/home_gallery_widget.dart b/mobile/lib/ui/home/home_gallery_widget.dart index 35bc5eb326..0ece05ba72 100644 --- a/mobile/lib/ui/home/home_gallery_widget.dart +++ b/mobile/lib/ui/home/home_gallery_widget.dart @@ -88,6 +88,7 @@ class _HomeGalleryWidgetState extends State { dedupeUploadID: true, ignoredCollectionIDs: collectionsToHide, ignoreSavedFiles: true, + ignoreSharedItems: _shouldHideSharedItems, ); if (hasSelectedAllForBackup) { result = await FilesDB.instance.getAllLocalAndUploadedFiles( @@ -97,7 +98,6 @@ class _HomeGalleryWidgetState extends State { limit: limit, asc: asc, filterOptions: filterOptions, - ignoreSharedFiles: _shouldHideSharedItems, ); } else { result = await FilesDB.instance.getAllPendingOrUploadedFiles( @@ -107,7 +107,6 @@ class _HomeGalleryWidgetState extends State { limit: limit, asc: asc, filterOptions: filterOptions, - ignoreSharedFiles: _shouldHideSharedItems, ); } diff --git a/mobile/lib/ui/home/landing_page_widget.dart b/mobile/lib/ui/home/landing_page_widget.dart index dda991b344..709124847b 100644 --- a/mobile/lib/ui/home/landing_page_widget.dart +++ b/mobile/lib/ui/home/landing_page_widget.dart @@ -42,10 +42,6 @@ class _LandingPageWidgetState extends State { void initState() { super.initState(); Future(_showAutoLogoutDialogIfRequired); - Future.delayed( - const Duration(seconds: 1), - _autoEnableResumableUpload, - ); } @override @@ -308,10 +304,6 @@ class _LandingPageWidgetState extends State { } } } - - void _autoEnableResumableUpload() { - localSettings.autoEnableMultiplePart(20).ignore(); - } } class FeatureItemWidget extends StatelessWidget { @@ -358,7 +350,10 @@ class FeatureItemWidget extends StatelessWidget { subText, textAlign: TextAlign.center, style: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.5), fontSize: 16, ), ), diff --git a/mobile/lib/ui/home/loading_photos_widget.dart b/mobile/lib/ui/home/loading_photos_widget.dart index d4eeb92650..12beccafb2 100644 --- a/mobile/lib/ui/home/loading_photos_widget.dart +++ b/mobile/lib/ui/home/loading_photos_widget.dart @@ -124,12 +124,12 @@ class _LoadingPhotosWidgetState extends State { isLightMode ? Image.asset( 'assets/loading_photos_background.png', - color: Colors.white.withOpacity(0.5), + color: Colors.white.withValues(alpha: 0.5), colorBlendMode: BlendMode.modulate, ) : Image.asset( 'assets/loading_photos_background_dark.png', - color: Colors.white.withOpacity(0.25), + color: Colors.white.withValues(alpha: 0.25), colorBlendMode: BlendMode.modulate, ), Column( diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 7a09c80f2a..5b82bb919f 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -185,7 +185,7 @@ class _FullScreenMemoryState extends State { currentStep: value + 1, size: 2, selectedColor: Colors.white, //same for both themes - unselectedColor: Colors.white.withOpacity(0.4), + unselectedColor: Colors.white.withValues(alpha: 0.4), ) : const SizedBox.shrink(), const SizedBox( @@ -217,8 +217,8 @@ class _FullScreenMemoryState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withOpacity(0.6), - Colors.black.withOpacity(0.5), + Colors.black.withValues(alpha: 0.6), + Colors.black.withValues(alpha: 0.5), Colors.transparent, ], stops: const [0, 0.6, 1], @@ -440,7 +440,7 @@ class BottomGradient extends StatelessWidget { begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ - Colors.black.withOpacity(0.5), //same for both themes + Colors.black.withValues(alpha: 0.5), //same for both themes Colors.transparent, ], stops: const [0, 0.8], diff --git a/mobile/lib/ui/home/memories/memory_cover_widget.dart b/mobile/lib/ui/home/memories/memory_cover_widget.dart index 55f8db652c..ef5b28e049 100644 --- a/mobile/lib/ui/home/memories/memory_cover_widget.dart +++ b/mobile/lib/ui/home/memories/memory_cover_widget.dart @@ -112,7 +112,7 @@ class _MemoryCoverWidgetState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.black.withOpacity(0.5), + Colors.black.withValues(alpha: 0.5), Colors.transparent, ], stops: const [0, 1], @@ -163,7 +163,7 @@ class _MemoryCoverWidgetState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.black.withOpacity(0.5), + Colors.black.withValues(alpha: 0.5), Colors.transparent, ], stops: const [0, 1], diff --git a/mobile/lib/ui/home/status_bar_widget.dart b/mobile/lib/ui/home/status_bar_widget.dart index 6ab6a682e1..f4cb9608ef 100644 --- a/mobile/lib/ui/home/status_bar_widget.dart +++ b/mobile/lib/ui/home/status_bar_widget.dart @@ -16,6 +16,7 @@ import 'package:photos/ui/account/verify_recovery_page.dart'; import 'package:photos/ui/components/home_header_widget.dart'; import 'package:photos/ui/components/notification_widget.dart'; import 'package:photos/ui/home/header_error_widget.dart'; +import "package:photos/ui/settings/backup/backup_settings_screen.dart"; import "package:photos/ui/settings/backup/backup_status_screen.dart"; import "package:photos/ui/settings/ml/enable_ml_consent.dart"; import 'package:photos/utils/navigation_util.dart'; @@ -34,7 +35,7 @@ class _StatusBarWidgetState extends State { late StreamSubscription _subscription; late StreamSubscription _notificationSubscription; - + bool _isPausedDueToNetwork = false; bool _showStatus = false; bool _showErrorBanner = false; bool _showMlBanner = !flagService.hasGrantedMLConsent && @@ -45,6 +46,7 @@ class _StatusBarWidgetState extends State { void initState() { _subscription = Bus.instance.on().listen((event) { _logger.info("Received event " + event.status.toString()); + _isPausedDueToNetwork = event.status == SyncStatus.paused; if (event.status == SyncStatus.error) { setState(() { _syncError = event.error; @@ -100,7 +102,9 @@ class _StatusBarWidgetState extends State { onTap: () { routeToPage( context, - const BackupStatusScreen(), + _isPausedDueToNetwork + ? const BackupSettingsScreen() + : const BackupStatusScreen(), forceCustomPageRoute: true, ).ignore(); }, diff --git a/mobile/lib/ui/map/map_view.dart b/mobile/lib/ui/map/map_view.dart index e25319b133..65b3967573 100644 --- a/mobile/lib/ui/map/map_view.dart +++ b/mobile/lib/ui/map/map_view.dart @@ -82,6 +82,7 @@ class _MapViewState extends State { } : null, initialCenter: widget.center, + backgroundColor: const Color.fromARGB(255, 246, 246, 246), minZoom: widget.minZoom, maxZoom: widget.maxZoom, interactionOptions: InteractionOptions( @@ -96,9 +97,7 @@ class _MapViewState extends State { ), ), onPositionChanged: (position, hasGesture) { - if (position.bounds != null) { - onChange(position.bounds!); - } + onChange(position.visibleBounds); }, ), children: [ diff --git a/mobile/lib/ui/map/tile/layers.dart b/mobile/lib/ui/map/tile/layers.dart index 2597dddea2..d3a28928dd 100644 --- a/mobile/lib/ui/map/tile/layers.dart +++ b/mobile/lib/ui/map/tile/layers.dart @@ -29,7 +29,6 @@ class OSMTileLayer extends StatelessWidget { return TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], - backgroundColor: Colors.transparent, userAgentPackageName: _userAgent, tileProvider: CachedNetworkTileProvider(), ); @@ -46,7 +45,6 @@ class OSMFranceTileLayer extends StatelessWidget { fallbackUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], tileProvider: CachedNetworkTileProvider(), - backgroundColor: const Color.fromARGB(255, 246, 246, 246), userAgentPackageName: _userAgent, panBuffer: 1, ); @@ -101,7 +99,6 @@ class MapBoxTilesLayer extends StatelessWidget { "https://api.mapbox.com/styles/v1/{mb_user}/{mb_style_id}/tiles/{z}/{x}/{y}?access_token={mb_token}", fallbackUrl: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], - backgroundColor: Colors.transparent, userAgentPackageName: _userAgent, tileProvider: CachedNetworkTileProvider(), additionalOptions: const { diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 2d115b3d88..84b95e3262 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; -import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -38,7 +37,7 @@ class _ChangeLogPageState extends State { padding: EdgeInsets.symmetric(horizontal: 16.0), child: TitleBarTitleWidget( // title: S.of(context).whatsNew, - title: "v1.0", + title: "v1.1.0", ), ), ), @@ -102,24 +101,28 @@ class _ChangeLogPageState extends State { final List items = []; items.addAll([ ChangeLogEntry( - context.l10n.cLIcon, - context.l10n.cLIconDesc, + "On This Day", + "You can now see a new memory called \"On This Day\" showing photos of a date across multiple years. Moreover, you will receive an opt-out notification every morning to check out this new memory.", ), ChangeLogEntry( - context.l10n.cLMemories, - context.l10n.cLMemoriesDesc, + "New Widgets", + "We have added new widgets for albums and people. You can customise which albums or people you want to see in these widgets as well.", ), ChangeLogEntry( - context.l10n.cLWidgets, - context.l10n.cLWidgetsDesc, + "Shareable Favorites", + "You can now share your favorites to your contacts just like any other album.", ), ChangeLogEntry( - context.l10n.cLFamilyPlan, - context.l10n.cLFamilyPlanDesc, + "Albums Improvements", + "We have redesigned the albums screen so you can select multiple albums and take actions like share, hide, archive, delete on all selected albums quickly.", ), ChangeLogEntry( - context.l10n.cLBulkEdit, - context.l10n.cLBulkEditDesc, + "Add to Multiple Albums", + "You can now select multiple albums when you want to add a photo to an album.", + ), + ChangeLogEntry( + "Albums in Contact Page", + "We have updated the contacts page to show you all the albums shared with you by the contact, so you can browse someone's shared library easily.", ), ]); diff --git a/mobile/lib/ui/payment/payment_web_page.dart b/mobile/lib/ui/payment/payment_web_page.dart index 23b1b66a74..6863697f1e 100644 --- a/mobile/lib/ui/payment/payment_web_page.dart +++ b/mobile/lib/ui/payment/payment_web_page.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -110,7 +109,7 @@ class _PaymentWebPageState extends State { }, ), ), - ].whereNotNull().toList(), + ].nonNulls.toList(), ), ), ); diff --git a/mobile/lib/ui/payment/subscription_plan_widget.dart b/mobile/lib/ui/payment/subscription_plan_widget.dart index 3a269ec2b5..52c4b925ba 100644 --- a/mobile/lib/ui/payment/subscription_plan_widget.dart +++ b/mobile/lib/ui/payment/subscription_plan_widget.dart @@ -60,7 +60,7 @@ class _SubscriptionPlanWidgetState extends State { : null, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.08), + color: Colors.black.withValues(alpha: 0.08), offset: const Offset(0, 4), blurRadius: 4, ), diff --git a/mobile/lib/ui/settings/about_section_widget.dart b/mobile/lib/ui/settings/about_section_widget.dart index 7ca9397274..decbbf023b 100644 --- a/mobile/lib/ui/settings/about_section_widget.dart +++ b/mobile/lib/ui/settings/about_section_widget.dart @@ -77,7 +77,7 @@ class AboutSectionWidget extends StatelessWidget { updateService.getLatestVersionInfo(), ); }, - barrierColor: Colors.black.withOpacity(0.85), + barrierColor: Colors.black.withValues(alpha: 0.85), ); } else { showShortToast( diff --git a/mobile/lib/ui/settings/account_section_widget.dart b/mobile/lib/ui/settings/account_section_widget.dart index a137349639..cd97cf2afa 100644 --- a/mobile/lib/ui/settings/account_section_widget.dart +++ b/mobile/lib/ui/settings/account_section_widget.dart @@ -71,7 +71,7 @@ class AccountSectionWidget extends StatelessWidget { builder: (BuildContext context) { return const ChangeEmailDialog(); }, - barrierColor: Colors.black.withOpacity(0.85), + barrierColor: Colors.black.withValues(alpha: 0.85), barrierDismissible: false, ); } diff --git a/mobile/lib/ui/settings/advanced_settings_screen.dart b/mobile/lib/ui/settings/advanced_settings_screen.dart index 4da030fdd2..d1b9ccfbd1 100644 --- a/mobile/lib/ui/settings/advanced_settings_screen.dart +++ b/mobile/lib/ui/settings/advanced_settings_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import "package:photos/core/error-reporting/super_logging.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/preview_video_store.dart"; +import "package:photos/services/video_preview_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; @@ -139,13 +139,13 @@ class AdvancedSettingsScreen extends StatelessWidget { singleBorderRadius: 8, alignCaptionedTextToLeft: true, trailingWidget: ToggleSwitchWidget( - value: () => PreviewVideoStore + value: () => VideoPreviewService .instance.isVideoStreamingEnabled, onChanged: () async { - final isEnabled = PreviewVideoStore + final isEnabled = VideoPreviewService .instance.isVideoStreamingEnabled; - await PreviewVideoStore.instance + await VideoPreviewService.instance .setIsVideoStreamingEnabled(!isEnabled); }, ), diff --git a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart index 3eea0c2e7a..896be6e15b 100644 --- a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart +++ b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart @@ -271,8 +271,11 @@ class _BackupFolderSelectionPageState extends State { final t = dragAnimation.value; final elevation = lerpDouble(0, 8, t)!; final themeColor = Theme.of(context).colorScheme.onSurface; - final color = - Color.lerp(themeColor, themeColor.withOpacity(0.8), t); + final color = Color.lerp( + themeColor, + themeColor.withValues(alpha: 0.8), + t, + ); return SizeFadeTransition( sizeFraction: 0.7, curve: Curves.easeInOut, @@ -359,7 +362,7 @@ class _BackupFolderSelectionPageState extends State { : Theme.of(context) .colorScheme .onSurface - .withOpacity(0.7), + .withValues(alpha: 0.7), ), overflow: TextOverflow.ellipsis, maxLines: 2, diff --git a/mobile/lib/ui/settings/backup/backup_item_card.dart b/mobile/lib/ui/settings/backup/backup_item_card.dart index c8f27b32bc..1c5f4fd2f9 100644 --- a/mobile/lib/ui/settings/backup/backup_item_card.dart +++ b/mobile/lib/ui/settings/backup/backup_item_card.dart @@ -1,12 +1,17 @@ import "dart:async"; import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; +import "package:photos/generated/l10n.dart"; import "package:photos/models/backup/backup_item.dart"; import "package:photos/models/backup/backup_item_status.dart"; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/ui/components/buttons/button_widget.dart"; +import "package:photos/ui/components/dialog_widget.dart"; +import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/viewer/file/detail_page.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; -import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/email_util.dart"; import "package:photos/utils/file_uploader.dart"; import "package:photos/utils/navigation_util.dart"; @@ -25,6 +30,7 @@ class BackupItemCard extends StatefulWidget { class _BackupItemCardState extends State { String? folderName; bool showThumbnail = false; + final _logger = Logger("BackupItemCard"); @override void initState() { @@ -71,7 +77,7 @@ class _BackupItemCardState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all( - color: colorScheme.fillFaint.withOpacity(0.08), + color: colorScheme.fillFaint.withValues(alpha: 0.08), width: 1, ), ), @@ -137,17 +143,40 @@ class _BackupItemCardState extends State { color: getEnteColorScheme(context).fillBase, ), onPressed: () { - String errorMessage = ""; - if (widget.item.error is Exception) { - final Exception ex = widget.item.error as Exception; - errorMessage = "Error: " + - ex.runtimeType.toString() + - " - " + - ex.toString(); - } else if (widget.item.error != null) { - errorMessage = widget.item.error.toString(); - } - showErrorDialog(context, 'Upload failed', errorMessage); + showDialogWidget( + context: context, + body: S.of(context).sorryBackupFailedDesc, + title: S.of(context).backupFailed, + icon: Icons.error_outline_outlined, + isDismissible: true, + buttons: [ + ButtonWidget( + buttonType: ButtonType.primary, + labelText: S.of(context).contactSupport, + buttonAction: ButtonAction.second, + onTap: () async { + _logger.warning( + "Backup failed for ${widget.item.file.displayName}", + widget.item.error, + ); + await sendLogs( + context, + S.of(context).contactSupport, + "support@ente.io", + postShare: () {}, + ); + }, + ), + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: S.of(context).ok, + buttonAction: ButtonAction.first, + onTap: () async { + Navigator.of(context).pop(); + }, + ), + ], + ); }, ), ), diff --git a/mobile/lib/ui/settings/backup/backup_status_screen.dart b/mobile/lib/ui/settings/backup/backup_status_screen.dart index 82471ef137..ed7865009e 100644 --- a/mobile/lib/ui/settings/backup/backup_status_screen.dart +++ b/mobile/lib/ui/settings/backup/backup_status_screen.dart @@ -138,8 +138,8 @@ class _BackupStatusScreenState extends State { fontSize: 16, height: 20 / 16, color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF000000).withOpacity(0.7) - : const Color(0xFFFFFFFF).withOpacity(0.7), + ? const Color(0xFF000000).withValues(alpha: 0.7) + : const Color(0xFFFFFFFF).withValues(alpha: 0.7), ), ), const SizedBox(height: 48), diff --git a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart index 1a78f7c851..913c248fe5 100644 --- a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart @@ -12,6 +12,7 @@ import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; import 'package:photos/services/machine_learning/ml_service.dart'; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; +import "package:photos/services/notification_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; @@ -66,6 +67,117 @@ class _MLDebugSectionWidgetState extends State { logger.info("Building ML Debug section options"); return Column( children: [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Show pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + final amount = + await NotificationService.instance.pendingNotifications(); + showShortToast(context, '$amount pending notifications'); + } catch (e, s) { + logger.severe('pendingNotifications failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Clear pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.clearAllScheduledNotifications( + containingPayload: "onThisDay", + ); + showShortToast(context, 'Done'); + } catch (e, s) { + logger.severe('clearAllScheduledNotifications failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 10 seconds", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + id: 10, + dateTime: DateTime.now().add( + const Duration(seconds: 10), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 1 hour", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + id: 11, + dateTime: DateTime.now().add( + const Duration(hours: 1), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Schedule notification 12 hours", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "test", + id: 12, + dateTime: DateTime.now().add( + const Duration(hours: 12), + ), + ); + showShortToast(context, 'done'); + } catch (e, s) { + logger.severe('schedule notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: FutureBuilder( @@ -328,7 +440,7 @@ class _MLDebugSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async => - await MemoryHomeWidgetService.instance.initMemoryHW(true), + await MemoryHomeWidgetService.instance.initMemoryHomeWidget(true), ), sectionOptionSpacing, MenuItemWidget( @@ -338,8 +450,8 @@ class _MLDebugSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () async => - await MemoryHomeWidgetService.instance.initMemoryHW(false), + onTap: () async => await MemoryHomeWidgetService.instance + .initMemoryHomeWidget(false), ), sectionOptionSpacing, MenuItemWidget( diff --git a/mobile/lib/ui/settings/developer_settings_page.dart b/mobile/lib/ui/settings/developer_settings_page.dart index 27267bf516..f78e96213a 100644 --- a/mobile/lib/ui/settings/developer_settings_page.dart +++ b/mobile/lib/ui/settings/developer_settings_page.dart @@ -65,7 +65,7 @@ class _DeveloperSettingsPageState extends State { showErrorDialog( context, S.of(context).invalidEndpoint, - S.of(context).invalidEndpointMessage, + S.of(context).invalidEndpointMessage + "\n" + e.toString(), ); } }, diff --git a/mobile/lib/ui/settings/gallery_settings_screen.dart b/mobile/lib/ui/settings/gallery_settings_screen.dart index 3e4e86e1d0..2fc5864cc3 100644 --- a/mobile/lib/ui/settings/gallery_settings_screen.dart +++ b/mobile/lib/ui/settings/gallery_settings_screen.dart @@ -109,7 +109,7 @@ class _GallerySettingsScreenState extends State { ); unawaited( MemoryHomeWidgetService.instance - .initMemoryHW(true), + .initMemoryHomeWidget(true), ); setState(() {}); }, diff --git a/mobile/lib/ui/settings/general_section_widget.dart b/mobile/lib/ui/settings/general_section_widget.dart index b5f7e35ad3..e4f5df18cc 100644 --- a/mobile/lib/ui/settings/general_section_widget.dart +++ b/mobile/lib/ui/settings/general_section_widget.dart @@ -15,6 +15,7 @@ import 'package:photos/ui/settings/advanced_settings_screen.dart'; import 'package:photos/ui/settings/common_settings.dart'; import "package:photos/ui/settings/language_picker.dart"; import "package:photos/ui/settings/notification_settings_screen.dart"; +import "package:photos/ui/settings/widget_settings_screen.dart"; import 'package:photos/utils/navigation_util.dart'; class GeneralSectionWidget extends StatelessWidget { @@ -45,7 +46,6 @@ class GeneralSectionWidget extends StatelessWidget { routeToPage( context, const ReferralScreen(), - forceCustomPageRoute: true, ); }, ), @@ -98,6 +98,18 @@ class GeneralSectionWidget extends StatelessWidget { }, ), sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).widgets, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + _onWidgetsTapped(context); + }, + ), + sectionOptionSpacing, MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: S.of(context).advanced, @@ -128,6 +140,13 @@ class GeneralSectionWidget extends StatelessWidget { ); } + void _onWidgetsTapped(BuildContext context) { + routeToPage( + context, + const WidgetSettingsScreen(), + ); + } + void _onAdvancedTapped(BuildContext context) { routeToPage( context, diff --git a/mobile/lib/ui/settings/language_picker.dart b/mobile/lib/ui/settings/language_picker.dart index 86d9a9a674..009150afb9 100644 --- a/mobile/lib/ui/settings/language_picker.dart +++ b/mobile/lib/ui/settings/language_picker.dart @@ -98,11 +98,21 @@ class _ItemsWidgetState extends State { @override Widget build(BuildContext context) { items.clear(); + bool foundMatch = false; for (Locale locale in widget.supportedLocales) { + if (currentLocale == locale) { + foundMatch = true; + } items.add( _menuItemForPicker(locale), ); } + if (!foundMatch && kDebugMode) { + items.insert( + 0, + Text("(i) Locale : ${currentLocale.toString()}"), + ); + } items = addSeparators( items, DividerWidget( diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_password.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_password.dart index 13f0442f7a..2d33cf69c5 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_password.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_password.dart @@ -117,10 +117,10 @@ class _LockScreenConfirmPasswordState extends State { shape: BoxShape.circle, gradient: LinearGradient( colors: [ - Colors.grey.shade500.withOpacity(0.2), - Colors.grey.shade50.withOpacity(0.1), - Colors.grey.shade400.withOpacity(0.2), - Colors.grey.shade300.withOpacity(0.4), + Colors.grey.shade500.withValues(alpha: 0.2), + Colors.grey.shade50.withValues(alpha: 0.1), + Colors.grey.shade400.withValues(alpha: 0.2), + Colors.grey.shade300.withValues(alpha: 0.4), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart index ae3e04129c..d21aee66a8 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart @@ -101,10 +101,10 @@ class _LockScreenConfirmPinState extends State { shape: BoxShape.circle, gradient: LinearGradient( colors: [ - Colors.grey.shade500.withOpacity(0.2), - Colors.grey.shade50.withOpacity(0.1), - Colors.grey.shade400.withOpacity(0.2), - Colors.grey.shade300.withOpacity(0.4), + Colors.grey.shade500.withValues(alpha: 0.2), + Colors.grey.shade50.withValues(alpha: 0.1), + Colors.grey.shade400.withValues(alpha: 0.2), + Colors.grey.shade300.withValues(alpha: 0.4), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart index 3c8c2cc330..b62e64c6f6 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart @@ -127,10 +127,10 @@ class _LockScreenPasswordState extends State { shape: BoxShape.circle, gradient: LinearGradient( colors: [ - Colors.grey.shade500.withOpacity(0.2), - Colors.grey.shade50.withOpacity(0.1), - Colors.grey.shade400.withOpacity(0.2), - Colors.grey.shade300.withOpacity(0.4), + Colors.grey.shade500.withValues(alpha: 0.2), + Colors.grey.shade50.withValues(alpha: 0.1), + Colors.grey.shade400.withValues(alpha: 0.2), + Colors.grey.shade300.withValues(alpha: 0.4), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart index 039b9df242..7c37f9c6ac 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart @@ -178,10 +178,10 @@ class _LockScreenPinState extends State { shape: BoxShape.circle, gradient: LinearGradient( colors: [ - Colors.grey.shade500.withOpacity(0.2), - Colors.grey.shade50.withOpacity(0.1), - Colors.grey.shade400.withOpacity(0.2), - Colors.grey.shade300.withOpacity(0.4), + Colors.grey.shade500.withValues(alpha: 0.2), + Colors.grey.shade50.withValues(alpha: 0.1), + Colors.grey.shade400.withValues(alpha: 0.2), + Colors.grey.shade300.withValues(alpha: 0.4), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, diff --git a/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart b/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart index 5db7a028ac..82c81e3967 100644 --- a/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart @@ -54,7 +54,7 @@ class _MachineLearningSettingsPageState enable: true, wakeLockFor: WakeLockFor.machineLearningSettingsScreen, ); - machineLearningController.forceOverrideML(turnOn: true); + computeController.forceOverrideML(turnOn: true); if (!MLIndexingIsolate.instance.areModelsDownloaded) { _timer = Timer.periodic(const Duration(seconds: 10), (timer) { if (mounted) { @@ -74,7 +74,7 @@ class _MachineLearningSettingsPageState enable: false, wakeLockFor: WakeLockFor.machineLearningSettingsScreen, ); - machineLearningController.forceOverrideML(turnOn: false); + computeController.forceOverrideML(turnOn: false); _timer?.cancel(); _advancedOptionsTimer?.cancel(); } @@ -429,13 +429,13 @@ class MLStatusWidget extends StatefulWidget { class MLStatusWidgetState extends State { Timer? _timer; - bool _isDeviceHealthy = machineLearningController.isDeviceHealthy; + bool _isDeviceHealthy = computeController.isDeviceHealthy; @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 10), (timer) { MLService.instance.triggerML(); - _isDeviceHealthy = machineLearningController.isDeviceHealthy; + _isDeviceHealthy = computeController.isDeviceHealthy; setState(() {}); }); } diff --git a/mobile/lib/ui/settings/notification_settings_screen.dart b/mobile/lib/ui/settings/notification_settings_screen.dart index f6dfc3eabc..9d0761b883 100644 --- a/mobile/lib/ui/settings/notification_settings_screen.dart +++ b/mobile/lib/ui/settings/notification_settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/notification_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; @@ -9,6 +10,7 @@ import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; +import "package:photos/ui/settings/common_settings.dart"; class NotificationSettingsScreen extends StatelessWidget { const NotificationSettingsScreen({super.key}); @@ -77,6 +79,60 @@ class NotificationSettingsScreen extends StatelessWidget { .of(context) .sharedPhotoNotificationsExplanation, ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).onThisDayMemories, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => + NotificationService.instance + .hasGrantedPermissions() && + localSettings + .isOnThisDayNotificationsEnabled, + onChanged: () async { + await NotificationService.instance + .requestPermissions(); + await memoriesCacheService + .toggleOnThisDayNotifications(); + }, + ), + singleBorderRadius: 8, + alignCaptionedTextToLeft: true, + isGestureDetectorDisabled: true, + ), + MenuSectionDescriptionWidget( + content: S + .of(context) + .onThisDayNotificationExplanation, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).birthdayNotifications, + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => + NotificationService.instance + .hasGrantedPermissions() && + localSettings.birthdayNotificationsEnabled, + onChanged: () async { + await NotificationService.instance + .requestPermissions(); + await memoriesCacheService + .toggleBirthdayNotifications(); + }, + ), + singleBorderRadius: 8, + alignCaptionedTextToLeft: true, + isGestureDetectorDisabled: true, + ), + MenuSectionDescriptionWidget( + content: + S.of(context).receiveRemindersOnBirthdays, + ), ], ), ], diff --git a/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart b/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart index e00dce529b..4c51486d57 100644 --- a/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart +++ b/mobile/lib/ui/settings/pending_sync/pending_sync_info_screen.dart @@ -82,6 +82,12 @@ class _PendingSyncInfoScreenState extends State { ".decrypted", allowCacheClear: false, ), + PathInfoStorageItem.name( + tempDownload, + "Partial Download", + "_part", + allowCacheClear: true, + ), PathInfoStorageItem.name( tempDownload, "Video Preview", diff --git a/mobile/lib/ui/settings/widget_settings_screen.dart b/mobile/lib/ui/settings/widget_settings_screen.dart new file mode 100644 index 0000000000..91a51db353 --- /dev/null +++ b/mobile/lib/ui/settings/widget_settings_screen.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import "package:flutter_svg/flutter_svg.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/settings/ml/enable_ml_consent.dart"; +import "package:photos/ui/settings/widgets/albums_widget_settings.dart"; +import "package:photos/ui/settings/widgets/memories_widget_settings.dart"; +import "package:photos/ui/settings/widgets/people_widget_settings.dart"; +import "package:photos/utils/navigation_util.dart"; + +class WidgetSettingsScreen extends StatelessWidget { + const WidgetSettingsScreen({super.key}); + + void onPeopleTapped(BuildContext context) { + final bool isMLEnabled = !flagService.hasGrantedMLConsent; + if (isMLEnabled) { + routeToPage( + context, + const EnableMachineLearningConsent(), + forceCustomPageRoute: true, + ); + return; + } + routeToPage( + context, + const PeopleWidgetSettings(), + ); + } + + void onAlbumsTapped(BuildContext context) { + routeToPage( + context, + const AlbumsWidgetSettings(), + ); + } + + void onMemoriesTapped(BuildContext context) { + routeToPage( + context, + const MemoriesWidgetSettings(), + ); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).widgets, + ), + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).people, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/people-widget-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onPeopleTapped(context), + ), + const SizedBox(height: 8), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).albums, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/albums-widget-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onAlbumsTapped(context), + ), + const SizedBox(height: 8), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).memories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/memories-widget-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right_outlined, + onTap: () async => onMemoriesTapped(context), + ), + ], + ), + ], + ), + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/albums_widget_settings.dart b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart new file mode 100644 index 0000000000..03ec9e9b42 --- /dev/null +++ b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/collection/collection.dart"; +import "package:photos/models/selected_albums.dart"; +import "package:photos/services/album_home_widget_service.dart"; +import "package:photos/services/collections_service.dart"; +import "package:photos/services/favorites_service.dart"; +import "package:photos/ui/collections/flex_grid_view.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/models/button_type.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; + +class AlbumsWidgetSettings extends StatefulWidget { + const AlbumsWidgetSettings({super.key}); + + @override + State createState() => _AlbumsWidgetSettingsState(); +} + +class _AlbumsWidgetSettingsState extends State { + final _selectedAlbums = SelectedAlbums(); + bool hasInstalledAny = false; + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + checkIfAnyWidgetInstalled(); + selectExisting(); + } + + Future checkIfAnyWidgetInstalled() async { + final count = await AlbumHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + Future selectExisting() async { + final selectedAlbums = + AlbumHomeWidgetService.instance.getSelectedAlbumIds(); + final albums = {}; + + if (selectedAlbums != null) { + for (final collectionID in selectedAlbums) { + final collection = + CollectionsService.instance.getCollectionByID(collectionID); + + if (collection != null) { + albums.add(collection); + } + } + } + + if (albums.isEmpty) { + final favorites = + await FavoritesService.instance.getFavoritesCollection(); + + if (favorites == null) { + return; + } + + albums.add(favorites); + } + + _selectedAlbums.select(albums); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: hasInstalledAny + ? Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8 + MediaQuery.viewPaddingOf(context).bottom, + ), + child: ListenableBuilder( + listenable: _selectedAlbums, + builder: (context, _) { + return ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).save, + shouldSurfaceExecutionStates: false, + onTap: _selectedAlbums.albums.isNotEmpty + ? () async { + final albums = _selectedAlbums.albums + .map((e) => e.id.toString()) + .toList(); + await AlbumHomeWidgetService.instance + .setSelectedAlbums(albums); + Navigator.pop(context); + await AlbumHomeWidgetService.instance + .albumsChanged(); + } + : null, + isDisabled: _selectedAlbums.albums.isEmpty, + ); + }, + ), + ) + : null, + body: Scrollbar( + interactive: true, + controller: _scrollController, + child: CustomScrollView( + controller: _scrollController, + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).albums, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).albumsWidgetDesc + : context.l10n.addAlbumWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/albums-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + FutureBuilder>( + future: + CollectionsService.instance.getCollectionForOnEnteSection(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + for (final collection in snapshot.data!) { + if (_selectedAlbums.albums.contains(collection)) { + data.remove(collection); + data.insert(0, collection); + } + } + + return CollectionsFlexiGridViewWidget( + data, + displayLimitCount: snapshot.data!.length, + shrinkWrap: false, + selectedAlbums: _selectedAlbums, + shouldShowCreateAlbum: false, + enableSelectionMode: true, + onlyAllowSelection: true, + ); + } else if (snapshot.hasError) { + return SliverToBoxAdapter( + child: Text(snapshot.error.toString()), + ); + } else { + return const SliverToBoxAdapter(child: EnteLoadingWidget()); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/memories_widget_settings.dart b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart new file mode 100644 index 0000000000..90c62c94ba --- /dev/null +++ b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import "package:flutter_svg/flutter_svg.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/memory_home_widget_service.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/captioned_text_widget.dart"; +import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/components/toggle_switch_widget.dart"; + +class MemoriesWidgetSettings extends StatefulWidget { + const MemoriesWidgetSettings({super.key}); + + @override + State createState() => _MemoriesWidgetSettingsState(); +} + +class _MemoriesWidgetSettingsState extends State { + bool hasInstalledAny = false; + + bool? isYearlyMemoriesEnabled = true; + bool? isSmartMemoriesEnabled = false; + bool? isOnThisDayMemoriesEnabled = false; + + late final bool isMLEnabled; + + @override + void initState() { + super.initState(); + + initVariables(); + checkIfAnyWidgetInstalled(); + } + + Future checkIfAnyWidgetInstalled() async { + final count = await MemoryHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + Future initVariables() async { + isMLEnabled = flagService.hasGrantedMLConsent; + isYearlyMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedLastYearMemories(); + isSmartMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedMLMemories(); + isOnThisDayMemoriesEnabled = + await MemoryHomeWidgetService.instance.getSelectedOnThisDayMemories(); + + if (isYearlyMemoriesEnabled == null || + isSmartMemoriesEnabled == null || + isOnThisDayMemoriesEnabled == null) { + if (isMLEnabled) { + enableMLMemories(); + } else { + enableNonMLMemories(); + } + } + + setState(() {}); + } + + void enableMLMemories() { + isYearlyMemoriesEnabled = false; + isSmartMemoriesEnabled = true; + isOnThisDayMemoriesEnabled = false; + } + + void enableNonMLMemories() { + isYearlyMemoriesEnabled = true; + isSmartMemoriesEnabled = false; + isOnThisDayMemoriesEnabled = true; + } + + Future updateVariables() async { + await MemoryHomeWidgetService.instance + .setSelectedLastYearMemories(isYearlyMemoriesEnabled!); + await MemoryHomeWidgetService.instance + .setSelectedMLMemories(isSmartMemoriesEnabled!); + await MemoryHomeWidgetService.instance + .setSelectedOnThisDayMemories(isOnThisDayMemoriesEnabled!); + await MemoryHomeWidgetService.instance.memoryChanged(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).memories, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).memoriesWidgetDesc + : context.l10n.addMemoriesWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/memories-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 18), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).pastYearsMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/past-year-memory-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isYearlyMemoriesEnabled ?? true, + onChanged: () async { + setState(() { + isYearlyMemoriesEnabled = + !isYearlyMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + const SizedBox(height: 4), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).onThisDayMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/memories-widget-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isOnThisDayMemoriesEnabled!, + onChanged: () async { + setState(() { + isOnThisDayMemoriesEnabled = + !isOnThisDayMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + if (isMLEnabled) ...[ + const SizedBox(height: 4), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).smartMemories, + ), + leadingIconWidget: SvgPicture.asset( + "assets/icons/smart-memory-icon.svg", + colorFilter: ColorFilter.mode( + colorScheme.textBase, + BlendMode.srcIn, + ), + ), + menuItemColor: colorScheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => isSmartMemoriesEnabled!, + onChanged: () async { + setState(() { + isSmartMemoriesEnabled = + !isSmartMemoriesEnabled!; + }); + await updateVariables(); + }, + ), + singleBorderRadius: 8, + isGestureDetectorDisabled: true, + ), + ], + ], + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/settings/widgets/people_widget_settings.dart b/mobile/lib/ui/settings/widgets/people_widget_settings.dart new file mode 100644 index 0000000000..564aa736e5 --- /dev/null +++ b/mobile/lib/ui/settings/widgets/people_widget_settings.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/selected_people.dart"; +import "package:photos/services/people_home_widget_service.dart"; +import "package:photos/ui/components/buttons/button_widget.dart"; +import 'package:photos/ui/components/buttons/icon_button_widget.dart'; +import "package:photos/ui/components/models/button_type.dart"; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import "package:photos/ui/viewer/search/result/people_section_all_page.dart"; + +class PeopleWidgetSettings extends StatefulWidget { + const PeopleWidgetSettings({super.key}); + + @override + State createState() => _PeopleWidgetSettingsState(); +} + +class _PeopleWidgetSettingsState extends State { + bool hasInstalledAny = false; + final _selectedPeople = SelectedPeople(); + + @override + void initState() { + super.initState(); + getSelections(); + checkIfAnyWidgetInstalled(); + } + + Future getSelections() async { + final selectedPeople = PeopleHomeWidgetService.instance.getSelectedPeople(); + + if (selectedPeople != null) { + _selectedPeople.select(selectedPeople.toSet()); + } + } + + Future checkIfAnyWidgetInstalled() async { + final count = await PeopleHomeWidgetService.instance.countHomeWidgets(); + setState(() { + hasInstalledAny = count > 0; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: hasInstalledAny + ? Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8 + MediaQuery.viewPaddingOf(context).bottom, + ), + child: ListenableBuilder( + listenable: _selectedPeople, + builder: (context, _) { + return ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).save, + shouldSurfaceExecutionStates: false, + isDisabled: _selectedPeople.personIds.isEmpty, + onTap: _selectedPeople.personIds.isEmpty + ? null + : () async { + await PeopleHomeWidgetService.instance + .setSelectedPeople( + _selectedPeople.personIds.toList(), + ); + Navigator.pop(context); + await PeopleHomeWidgetService.instance + .peopleChanged(); + }, + ); + }, + ), + ) + : null, + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: S.of(context).people, + ), + expandedHeight: 120, + flexibleSpaceCaption: hasInstalledAny + ? S.of(context).peopleWidgetDesc + : context.l10n.addPeopleWidgetPrompt, + actionIcons: [ + IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.secondary, + onTap: () { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ), + if (!hasInstalledAny) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.5 - 200, + ), + Image.asset( + "assets/people-widget-static.png", + height: 160, + ), + ], + ), + ), + ) + else + SliverFillRemaining( + child: PeopleSectionAllWidget( + selectedPeople: _selectedPeople, + namedOnly: true, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/ui/sharing/add_participant_page.dart b/mobile/lib/ui/sharing/add_participant_page.dart index 6c2ffbef0d..c16931b121 100644 --- a/mobile/lib/ui/sharing/add_participant_page.dart +++ b/mobile/lib/ui/sharing/add_participant_page.dart @@ -18,12 +18,26 @@ import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/notification/toast.dart"; import 'package:photos/ui/sharing/user_avator_widget.dart'; import "package:photos/ui/sharing/verify_identity_dialog.dart"; +import "package:photos/utils/separators_util.dart"; + +enum ActionTypesToShow { + addViewer, + addCollaborator, +} class AddParticipantPage extends StatefulWidget { - final Collection collection; - final bool isAddingViewer; + /// Cannot be empty + final List actionTypesToShow; + final List collections; - const AddParticipantPage(this.collection, this.isAddingViewer, {super.key}); + AddParticipantPage( + this.collections, + this.actionTypesToShow, { + super.key, + }) : assert( + actionTypesToShow.isNotEmpty, + 'actionTypesToShow cannot be empty', + ); @override State createState() => _AddParticipantPage(); @@ -72,9 +86,7 @@ class _AddParticipantPage extends State { resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( title: Text( - widget.isAddingViewer - ? S.of(context).addViewer - : S.of(context).addCollaborator, + _getTitle(), ), ), body: Column( @@ -124,13 +136,15 @@ class _AddParticipantPage extends State { .longPressAnEmailToVerifyEndToEndEncryption, ) : const SizedBox.shrink(), - widget.isAddingViewer - ? const SizedBox.shrink() - : MenuSectionDescriptionWidget( + widget.actionTypesToShow.contains( + ActionTypesToShow.addCollaborator, + ) + ? MenuSectionDescriptionWidget( content: S .of(context) .collaboratorsCanAddPhotosAndVideosToTheSharedAlbum, - ), + ) + : const SizedBox.shrink(), ], ), ); @@ -216,48 +230,7 @@ class _AddParticipantPage extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.primary, - buttonSize: ButtonSize.large, - labelText: widget.isAddingViewer - ? S.of(context).addViewers(_selectedEmails.length) - : S - .of(context) - .addCollaborators(_selectedEmails.length), - isDisabled: _selectedEmails.isEmpty, - onTap: () async { - final results = []; - for (String email in _selectedEmails) { - results.add( - await collectionActions.addEmailToCollection( - context, - widget.collection, - email, - widget.isAddingViewer - ? CollectionParticipantRole.viewer - : CollectionParticipantRole.collaborator, - ), - ); - } - - final noOfSuccessfullAdds = - results.where((e) => e).length; - showToast( - context, - widget.isAddingViewer - ? S - .of(context) - .viewersSuccessfullyAdded(noOfSuccessfullAdds) - : S.of(context).collaboratorsSuccessfullyAdded( - noOfSuccessfullAdds, - ), - ); - - if (!results.any((e) => e == false) && mounted) { - Navigator.of(context).pop(true); - } - }, - ), + ..._actionButtons(), const SizedBox(height: 12), ], ), @@ -268,6 +241,100 @@ class _AddParticipantPage extends State { ); } + List _actionButtons() { + final widgets = []; + if (widget.actionTypesToShow.contains(ActionTypesToShow.addViewer)) { + widgets.add( + ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).addViewers(_selectedEmails.length), + isDisabled: _selectedEmails.isEmpty, + onTap: () async { + final results = []; + final collections = widget.collections; + + for (String email in _selectedEmails) { + bool result = false; + for (Collection collection in collections) { + result = await collectionActions.addEmailToCollection( + context, + collection, + email, + CollectionParticipantRole.viewer, + ); + } + results.add(result); + } + + final noOfSuccessfullAdds = results.where((e) => e).length; + showToast( + context, + S.of(context).viewersSuccessfullyAdded(noOfSuccessfullAdds), + ); + + if (!results.any((e) => e == false) && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + ); + } + if (widget.actionTypesToShow.contains( + ActionTypesToShow.addCollaborator, + )) { + widgets.add( + ButtonWidget( + buttonType: + widget.actionTypesToShow.contains(ActionTypesToShow.addViewer) + ? ButtonType.neutral + : ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: S.of(context).addCollaborators(_selectedEmails.length), + isDisabled: _selectedEmails.isEmpty, + onTap: () async { + // TODO: This is not currently designed for best UX for action on + // multiple collections and emails, especially if some operations + // fail. Can be improved by using a different 'addEmailToCollection' + // that accepts list of emails and list of collections. + final results = []; + final collections = widget.collections; + + for (String email in _selectedEmails) { + bool result = false; + for (Collection collection in collections) { + result = await collectionActions.addEmailToCollection( + context, + collection, + email, + CollectionParticipantRole.collaborator, + ); + } + results.add(result); + } + + final noOfSuccessfullAdds = results.where((e) => e).length; + showToast( + context, + S.of(context).collaboratorsSuccessfullyAdded(noOfSuccessfullAdds), + ); + + if (!results.any((e) => e == false) && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + ); + } + final widgetsWithSpaceBetween = addSeparators( + widgets, + const SizedBox( + height: 8, + ), + ); + return widgetsWithSpaceBetween; + } + void clearFocus() { _textController.clear(); _newEmail = _textController.text; @@ -363,9 +430,16 @@ class _AddParticipantPage extends State { List _getSuggestedUser() { final Set existingEmails = {}; - for (final User u in widget.collection.sharees) { - if (u.id != null && u.email.isNotEmpty) { - existingEmails.add(u.email); + final collections = widget.collections; + if (collections.isEmpty) { + return []; + } + + for (final Collection collection in collections) { + for (final User u in collection.sharees) { + if (u.id != null && u.email.isNotEmpty) { + existingEmails.add(u.email); + } } } @@ -385,4 +459,14 @@ class _AddParticipantPage extends State { return suggestedUsers; } + + String _getTitle() { + if (widget.actionTypesToShow.length > 1) { + return S.of(context).addParticipants; + } else if (widget.actionTypesToShow.first == ActionTypesToShow.addViewer) { + return S.of(context).addViewer; + } else { + return S.of(context).addCollaborator; + } + } } diff --git a/mobile/lib/ui/sharing/album_participants_page.dart b/mobile/lib/ui/sharing/album_participants_page.dart index e27fe92478..b162b1f8dd 100644 --- a/mobile/lib/ui/sharing/album_participants_page.dart +++ b/mobile/lib/ui/sharing/album_participants_page.dart @@ -54,7 +54,12 @@ class _AlbumParticipantsPageState extends State { Future _navigateToAddUser(bool addingViewer) async { await routeToPage( context, - AddParticipantPage(widget.collection, addingViewer), + AddParticipantPage( + [widget.collection], + addingViewer + ? [ActionTypesToShow.addViewer] + : [ActionTypesToShow.addCollaborator], + ), ); if (mounted) { setState(() => {}); diff --git a/mobile/lib/ui/sharing/share_collection_page.dart b/mobile/lib/ui/sharing/share_collection_page.dart index 64090528c8..0b1ab33bb9 100644 --- a/mobile/lib/ui/sharing/share_collection_page.dart +++ b/mobile/lib/ui/sharing/share_collection_page.dart @@ -92,7 +92,10 @@ class _ShareCollectionPageState extends State { // ignore: unawaited_futures routeToPage( context, - AddParticipantPage(widget.collection, true), + AddParticipantPage( + [widget.collection], + const [ActionTypesToShow.addViewer], + ), ).then( (value) => { if (mounted) {setState(() => {})}, @@ -118,8 +121,13 @@ class _ShareCollectionPageState extends State { isTopBorderRadiusRemoved: true, onTap: () async { // ignore: unawaited_futures - routeToPage(context, AddParticipantPage(widget.collection, false)) - .then( + routeToPage( + context, + AddParticipantPage( + [widget.collection], + const [ActionTypesToShow.addCollaborator], + ), + ).then( (value) => { if (mounted) {setState(() => {})}, }, diff --git a/mobile/lib/ui/sharing/user_avator_widget.dart b/mobile/lib/ui/sharing/user_avator_widget.dart index 7acd869f87..b77ba2d2a0 100644 --- a/mobile/lib/ui/sharing/user_avator_widget.dart +++ b/mobile/lib/ui/sharing/user_avator_widget.dart @@ -8,11 +8,10 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/user.dart"; -import "package:photos/models/file/file.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; -import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +import "package:photos/ui/viewer/people/person_face_widget.dart"; import "package:photos/utils/standalone/debouncer.dart"; import 'package:tuple/tuple.dart'; @@ -39,7 +38,7 @@ class UserAvatarWidget extends StatefulWidget { class _UserAvatarWidgetState extends State { Future? _personID; - EnteFile? _faceThumbnail; + bool _canUsePersonFaceWidget = false; final _logger = Logger("_UserAvatarWidgetState"); late final StreamSubscription _peopleChangedSubscription; final _debouncer = Debouncer( @@ -78,10 +77,7 @@ class _UserAvatarWidgetState extends State { final person = people.firstWhereOrNull( (person) => person.data.email == widget.user.email, ); - if (person != null) { - _faceThumbnail = - await PersonService.instance.getThumbnailFileOfPerson(person); - } + _canUsePersonFaceWidget = person != null; return person?.remoteID; }); } else { @@ -117,24 +113,23 @@ class _UserAvatarWidgetState extends State { if (snapshot.hasData) { final personID = snapshot.data as String; return ClipOval( - child: _faceThumbnail == null - ? _FirstLetterCircularAvatar( - user: widget.user, - currentUserID: widget.currentUserID, - thumbnailView: widget.thumbnailView, - type: widget.type, - ) - : PersonFaceWidget( - _faceThumbnail!, + child: _canUsePersonFaceWidget + ? PersonFaceWidget( personId: personID, onErrorCallback: () { if (mounted) { setState(() { _personID = null; - _faceThumbnail = null; + _canUsePersonFaceWidget = false; }); } }, + ) + : _FirstLetterCircularAvatar( + user: widget.user, + currentUserID: widget.currentUserID, + thumbnailView: widget.thumbnailView, + type: widget.type, ), ); } else if (snapshot.hasError) { diff --git a/mobile/lib/ui/tabs/home_widget.dart b/mobile/lib/ui/tabs/home_widget.dart index 1c2e8ca2da..22179985e6 100644 --- a/mobile/lib/ui/tabs/home_widget.dart +++ b/mobile/lib/ui/tabs/home_widget.dart @@ -32,14 +32,17 @@ import "package:photos/l10n/l10n.dart"; import "package:photos/models/collection/collection.dart"; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/models/file/file.dart"; +import "package:photos/models/selected_albums.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/account/user_service.dart'; +import "package:photos/services/album_home_widget_service.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; import "package:photos/services/notification_service.dart"; +import "package:photos/services/people_home_widget_service.dart"; import "package:photos/services/sync/diff_fetcher.dart"; import 'package:photos/services/sync/local_sync_service.dart'; import "package:photos/services/sync/remote_sync_service.dart"; @@ -64,6 +67,7 @@ import "package:photos/ui/settings_page.dart"; import "package:photos/ui/tabs/shared_collections_tab.dart"; import "package:photos/ui/tabs/user_collections_tab.dart"; import "package:photos/ui/viewer/actions/file_viewer.dart"; +import "package:photos/ui/viewer/file/detail_page.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; import "package:photos/ui/viewer/gallery/shared_public_collection_page.dart"; import "package:photos/ui/viewer/search/search_widget.dart"; @@ -83,7 +87,6 @@ class HomeWidget extends StatefulWidget { } class _HomeWidgetState extends State { - static const _userCollectionsTab = UserCollectionsTab(); static const _sharedCollectionTab = SharedCollectionsTab(); static const _searchTab = SearchTab(); static final _settingsPage = SettingsPage( @@ -91,6 +94,7 @@ class _HomeWidgetState extends State { ); final _logger = Logger("HomeWidgetState"); + final _selectedAlbums = SelectedAlbums(); final _selectedFiles = SelectedFiles(); final PageController _pageController = PageController(); @@ -125,6 +129,8 @@ class _HomeWidgetState extends State { if (LocalSyncService.instance.hasCompletedFirstImport()) { MemoryHomeWidgetService.instance.checkPendingMemorySync(); + PeopleHomeWidgetService.instance.checkPendingPeopleSync(); + AlbumHomeWidgetService.instance.checkPendingAlbumsSync(); } _tabChangedEventSubscription = Bus.instance.on().listen((event) { @@ -189,6 +195,8 @@ class _HomeWidgetState extends State { if (mounted) { setState(() {}); MemoryHomeWidgetService.instance.checkPendingMemorySync(); + AlbumHomeWidgetService.instance.checkPendingAlbumsSync(); + PeopleHomeWidgetService.instance.checkPendingPeopleSync(); } }, ); @@ -224,7 +232,7 @@ class _HomeWidgetState extends State { updateService.getLatestVersionInfo(), ); }, - barrierColor: Colors.black.withOpacity(0.85), + barrierColor: Colors.black.withValues(alpha: 0.85), ); updateService.resetUpdateAvailableShownTime(); } @@ -365,7 +373,7 @@ class _HomeWidgetState extends State { ); await dialog.hide(); - await routeToPage( + routeToPage( context, SharedPublicCollectionPage( files: sharedFiles, @@ -374,7 +382,19 @@ class _HomeWidgetState extends State { null, ), ), - ); + ).ignore(); + if (sharedFiles.length == 1) { + await routeToPage( + context, + DetailPage( + DetailPageConfiguration( + sharedFiles, + 0, + "sharedPublicCollection", + ), + ), + ); + } } } catch (e, s) { _logger.severe("Failed to handle public album link", e, s); @@ -441,15 +461,18 @@ class _HomeWidgetState extends State { _intentDataStreamSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( (List value) { - if (value.isNotEmpty && value[0].path.contains("albums.ente.io")) { + if (value.isEmpty) { + return; + } + if (value[0].path.contains("albums.ente.io")) { final uri = Uri.parse(value[0].path); _handlePublicAlbumLink(uri); return; } - if (value.isNotEmpty && - (value[0].mimeType == "image/*" || - value[0].mimeType == "video/*")) { + if (value[0].mimeType != null && + (value[0].mimeType!.contains("image") || + value[0].mimeType!.contains("video"))) { showDialog( context: context, builder: (BuildContext context) { @@ -601,10 +624,16 @@ class _HomeWidgetState extends State { } else { Navigator.pop(context); } - } else { - Bus.instance - .fire(TabChangedEvent(0, TabChangedEventSource.backButton)); + return; } + if (_selectedTabIndex == 1) { + if (_selectedAlbums.albums.isNotEmpty) { + _selectedAlbums.clearAll(); + return; + } + } + Bus.instance + .fire(TabChangedEvent(0, TabChangedEventSource.backButton)); }, child: Scaffold( drawerScrimColor: getEnteColorScheme(context).strokeFainter, @@ -706,7 +735,7 @@ class _HomeWidgetState extends State { ), selectedFiles: _selectedFiles, ), - _userCollectionsTab, + UserCollectionsTab(selectedAlbums: _selectedAlbums), _sharedCollectionTab, _searchTab, ], @@ -753,6 +782,7 @@ class _HomeWidgetState extends State { : const SizedBox.shrink(), HomeBottomNavigationBar( _selectedFiles, + _selectedAlbums, selectedTabIndex: _selectedTabIndex, ), ], @@ -850,22 +880,31 @@ class _HomeWidgetState extends State { final String? payload = notificationResponse.payload; if (payload != null) { debugPrint('notification payload: $payload'); - final collectionID = Uri.parse(payload).queryParameters["collectionID"]; - if (collectionID != null) { - final collection = CollectionsService.instance - .getCollectionByID(int.parse(collectionID))!; - final thumbnail = - await CollectionsService.instance.getCover(collection); + if (payload.toLowerCase().contains("onthisday")) { // ignore: unawaited_futures - routeToPage( - context, - CollectionPage( - CollectionWithThumbnail( - collection, - thumbnail, + memoriesCacheService.goToOnThisDayMemory(context); + } else if (payload.toLowerCase().contains("birthday")) { + final personID = payload.substring("birthday_".length); + // ignore: unawaited_futures + memoriesCacheService.goToPersonMemory(context, personID); + } else { + final collectionID = Uri.parse(payload).queryParameters["collectionID"]; + if (collectionID != null) { + final collection = CollectionsService.instance + .getCollectionByID(int.parse(collectionID))!; + final thumbnail = + await CollectionsService.instance.getCover(collection); + // ignore: unawaited_futures + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail( + collection, + thumbnail, + ), ), - ), - ); + ); + } } } } diff --git a/mobile/lib/ui/tabs/nav_bar.dart b/mobile/lib/ui/tabs/nav_bar.dart index e5f0d22c2d..c5feae0e5c 100644 --- a/mobile/lib/ui/tabs/nav_bar.dart +++ b/mobile/lib/ui/tabs/nav_bar.dart @@ -1,5 +1,3 @@ -library google_nav_bar; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import "package:photos/theme/effects.dart"; @@ -340,7 +338,7 @@ class _ButtonState extends State - )} - - ); -} - -const DirectoryPath = ({ path }) => ( - void ensureElectron().openDirectory(path)}> - - - {path} - - - -); - -const ChangeDirectoryOption: React.FC = ({ onClick }) => ( - - }> - {t("change_folder")} - - -); - -function ContinuousExport({ continuousExport, toggleContinuousExport }) { - return ( - - - {t("sync_continuously")} - - - - - - ); -} - -const ExportDynamicContent = ({ - exportStage, - startExport, - stopExport, - onHide, - lastExportTime, - exportProgress, - pendingExports, - allCollectionsNameByID, -}: { - exportStage: ExportStage; - startExport: (opts?: ExportOpts) => void; - stopExport: () => void; - onHide: () => void; - lastExportTime: number; - exportProgress: ExportProgress; - pendingExports: EnteFile[]; - allCollectionsNameByID: Map; -}) => { - switch (exportStage) { - case ExportStage.init: - return ; - - case ExportStage.migration: - case ExportStage.starting: - case ExportStage.exportingFiles: - case ExportStage.renamingCollectionFolders: - case ExportStage.trashingDeletedFiles: - case ExportStage.trashingDeletedCollections: - return ( - - ); - case ExportStage.finished: - return ( - startExport({ resync: true })} - /> - ); - - default: - return <>; - } -}; diff --git a/web/apps/photos/src/components/ExportFinished.tsx b/web/apps/photos/src/components/ExportFinished.tsx deleted file mode 100644 index 5a87e612ac..0000000000 --- a/web/apps/photos/src/components/ExportFinished.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { DialogActions, DialogContent, Stack, Typography } from "@mui/material"; -import { LinkButton } from "ente-base/components/LinkButton"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { formattedNumber } from "ente-base/i18n"; -import { formattedDateTime } from "ente-base/i18n-date"; -import { EnteFile } from "ente-media/file"; -import { SpaceBetweenFlex } from "ente-shared/components/Container"; -import { t } from "i18next"; -import { useState } from "react"; -import ExportPendingList from "./ExportPendingList"; - -interface Props { - pendingExports: EnteFile[]; - allCollectionsNameByID: Map; - onHide: () => void; - lastExportTime: number; - /** Called when the user presses the "Resync" button. */ - onResync: () => void; -} - -export default function ExportFinished(props: Props) { - const { lastExportTime } = props; - - const [pendingFileListView, setPendingFileListView] = - useState(false); - - const openPendingFileList = () => { - setPendingFileListView(true); - }; - - const closePendingFileList = () => { - setPendingFileListView(false); - }; - return ( - <> - - - - - {t("pending_items")} - - {props.pendingExports.length ? ( - - {formattedNumber(props.pendingExports.length)} - - ) : ( - - {formattedNumber(props.pendingExports.length)} - - )} - - - - {t("last_export_time")} - - - {lastExportTime - ? formattedDateTime(new Date(lastExportTime)) - : t("never")} - - - - - - - {t("close")} - - - {t("export_again")} - - - - - ); -} diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx deleted file mode 100644 index 3f2566f38b..0000000000 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { - DialogActions, - DialogContent, - LinearProgress, - Typography, -} from "@mui/material"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import { ExportStage, type ExportProgress } from "services/export"; - -interface Props { - exportStage: ExportStage; - exportProgress: ExportProgress; - stopExport: () => void; - closeExportDialog: () => void; -} - -export default function ExportInProgress(props: Props) { - const showIndeterminateProgress = () => { - return ( - props.exportStage === ExportStage.starting || - props.exportStage === ExportStage.migration || - props.exportStage === ExportStage.renamingCollectionFolders || - props.exportStage === ExportStage.trashingDeletedFiles || - props.exportStage === ExportStage.trashingDeletedCollections - ); - }; - return ( - <> - - - - {props.exportStage === ExportStage.starting ? ( - t("export_starting") - ) : props.exportStage === ExportStage.migration ? ( - t("export_preparing") - ) : props.exportStage === - ExportStage.renamingCollectionFolders ? ( - t("export_renaming_album_folders") - ) : props.exportStage === - ExportStage.trashingDeletedFiles ? ( - t("export_trashing_deleted_files") - ) : props.exportStage === - ExportStage.trashingDeletedCollections ? ( - t("export_trashing_deleted_albums") - ) : ( - - - ), - }} - values={{ progress: props.exportProgress }} - /> - - )} - - - {showIndeterminateProgress() ? ( - - ) : ( - - )} - - - - - - {t("close")} - - - {t("stop")} - - - - ); -} diff --git a/web/apps/photos/src/components/ExportInit.tsx b/web/apps/photos/src/components/ExportInit.tsx deleted file mode 100644 index 4a6d271edf..0000000000 --- a/web/apps/photos/src/components/ExportInit.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { DialogActions, DialogContent } from "@mui/material"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { t } from "i18next"; - -interface Props { - startExport: () => void; -} -export default function ExportInit({ startExport }: Props) { - return ( - - - - {t("start")} - - - - ); -} diff --git a/web/apps/photos/src/components/ExportPendingList.tsx b/web/apps/photos/src/components/ExportPendingList.tsx deleted file mode 100644 index da7f65b8ca..0000000000 --- a/web/apps/photos/src/components/ExportPendingList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Box, styled } from "@mui/material"; -import ItemList from "components/ItemList"; -import { TitledMiniDialog } from "ente-base/components/MiniDialog"; -import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { EnteFile } from "ente-media/file"; -import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; -import { FlexWrapper } from "ente-shared/components/Container"; -import { t } from "i18next"; - -interface Iprops { - isOpen: boolean; - onClose: () => void; - allCollectionsNameByID: Map; - pendingExports: EnteFile[]; -} - -export const ItemContainer = styled("div")` - position: relative; - top: 5px; - display: inline-block; - max-width: 394px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -`; - -const ExportPendingList = (props: Iprops) => { - const renderListItem = (file: EnteFile) => { - return ( - - - - - - {`${props.allCollectionsNameByID.get(file.collectionID)} / ${ - file.metadata.title - }`} - - - ); - }; - - const getItemTitle = (file: EnteFile) => { - return `${props.allCollectionsNameByID.get(file.collectionID)} / ${ - file.metadata.title - }`; - }; - - const generateItemKey = (file: EnteFile) => { - return `${file.collectionID}-${file.id}`; - }; - - return ( - - - - {t("close")} - - - ); -}; - -export default ExportPendingList; diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index dfba4ab60c..54daf7a82d 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -6,9 +6,12 @@ import Avatar from "components/pages/gallery/Avatar"; import { assertionFailed } from "ente-base/assert"; import { Overlay } from "ente-base/components/containers"; import { isSameDay } from "ente-base/date"; +import { isDevBuild } from "ente-base/env"; import { formattedDateRelative } from "ente-base/i18n-date"; +import log from "ente-base/log"; import { downloadManager } from "ente-gallery/services/download"; import { EnteFile, enteFileDeletionDate } from "ente-media/file"; +import { fileDurationString } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { GAP_BTW_TILES, @@ -23,7 +26,6 @@ import { } from "ente-new/photos/components/PlaceholderThumbnails"; import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles"; import { TRASH_SECTION } from "ente-new/photos/services/collection"; -import { FlexWrapper } from "ente-shared/components/Container"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -123,6 +125,16 @@ export interface FileListProps { * Not set in the context of the shared albums app. */ favoriteFileIDs?: Set; + /** + * An optional {@link TimeStampListItem} shown before all the items in the + * list. It is not sticky, and scrolls along with the content of the list. + */ + header?: TimeStampListItem; + /** + * An optional {@link TimeStampListItem} shown after all the items in the + * list. It is not sticky, and scrolls along with the content of the list. + */ + footer?: TimeStampListItem; /** * Called when the user activates the thumbnail at the given {@link index}. * @@ -148,6 +160,8 @@ export const FileList: React.FC = ({ activeCollectionID, activePersonID, favoriteFileIDs, + header, + footer, onItemClick, }) => { const galleryContext = useContext(GalleryContext); @@ -195,7 +209,9 @@ export const FileList: React.FC = ({ refreshInProgress.current = true; let timeStampList: TimeStampListItem[] = []; - if (galleryContext.photoListHeader) { + if (header) { + timeStampList.push(asFullSpanListItem(header)); + } else if (galleryContext.photoListHeader) { timeStampList.push( getPhotoListHeader(galleryContext.photoListHeader), ); @@ -219,7 +235,9 @@ export const FileList: React.FC = ({ timeStampList.push(getEmptyListItem()); } timeStampList.push(getVacuumItem(timeStampList)); - if (publicCollectionGalleryContext.credentials) { + if (footer) { + timeStampList.push(asFullSpanListItem(footer)); + } else if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { timeStampList.push( getPhotoListFooter( @@ -256,7 +274,11 @@ export const FileList: React.FC = ({ if (hasHeader) { return timeStampList; } - if (galleryContext.photoListHeader) { + // TODO(RE): Remove after audit. + if (isDevBuild) throw new Error("Unexpected header change"); + if (header) { + return [asFullSpanListItem(header), ...timeStampList]; + } else if (galleryContext.photoListHeader) { return [ getPhotoListHeader(galleryContext.photoListHeader), ...timeStampList, @@ -284,10 +306,27 @@ export const FileList: React.FC = ({ timeStampList.length > 0 && timeStampList[timeStampList.length - 1]?.tag == "publicAlbumsFooter"; + if (hasFooter) { return timeStampList; } - if (publicCollectionGalleryContext.credentials) { + // TODO(RE): Remove after audit. + if ( + isDevBuild && + (footer || + publicCollectionGalleryContext.credentials || + showAppDownloadBanner) + ) { + console.log({ timeStampList, footer, showAppDownloadBanner }); + throw new Error("Unexpected footer change"); + } + if (footer) { + return [ + ...timeStampList, + asFullSpanListItem(footer), + getAlbumsFooter(), + ]; + } else if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { return [ ...timeStampList, @@ -979,10 +1018,23 @@ const ListContainer = styled(Box, { } `; -const ListItemContainer = styled(FlexWrapper)<{ span: number }>` +const ListItemContainer = styled("div")<{ span: number }>` grid-column: span ${(props) => props.span}; + display: flex; + align-items: center; `; +const FullSpanListItemContainer = styled("div")` + grid-column: 1 / -1; + display: flex; + align-items: center; +`; + +const asFullSpanListItem = ({ item, ...rest }: TimeStampListItem) => ({ + ...rest, + item: {item}, +}); + const DateContainer = styled(ListItemContainer)( ({ theme }) => ` white-space: nowrap; @@ -1141,7 +1193,10 @@ const FileThumbnail: React.FC = ({ void downloadManager .renderableThumbnailURL(file, showPlaceholder) - .then((url) => !didCancel && setImageURL(url)); + .then((url) => !didCancel && setImageURL(url)) + .catch((e: unknown) => { + log.warn("Failed to fetch thumbnail", e); + }); return () => { didCancel = true; @@ -1180,7 +1235,7 @@ const FileThumbnail: React.FC = ({ onClick={handleClick} onMouseEnter={handleHover} disabled={!imageURL} - {...(selectable ? longPressHandlers : {})} + {...(selectable && longPressHandlers)} > {selectable && ( = ({ ) : ( )} - {file.metadata.fileType === FileType.livePhoto ? ( + {file.metadata.fileType == FileType.livePhoto ? ( - + ) : ( - file.metadata.fileType === FileType.video && ( - - - + file.metadata.fileType == FileType.video && ( + ) )} {selected && } @@ -1400,3 +1453,19 @@ const SelectedOverlay = styled(Overlay)( border-radius: 4px; `, ); + +interface VideoDurationOverlayProps { + duration: string | undefined; +} + +const VideoDurationOverlay: React.FC = ({ + duration, +}) => ( + + {duration ? ( + {duration} + ) : ( + + )} + +); diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index 9725376b52..e071c99c52 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -14,7 +14,7 @@ import { import { t } from "i18next"; import { useCallback, useMemo, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; -import uploadManager from "services/upload/uploadManager"; +import { uploadManager } from "services/upload-manager"; import { SetFilesDownloadProgressAttributesCreator } from "types/gallery"; import { downloadSingleFile } from "utils/file"; import { diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx index 396ecef737..03b3faf050 100644 --- a/web/apps/photos/src/components/FixCreationTime.tsx +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -89,7 +89,7 @@ export const FixCreationTime: React.FC = ({ pt: "6px", display: "flex", flexDirection: "column", - ...(step == "running" ? { alignItems: "center" } : {}), + ...(step == "running" && { alignItems: "center" }), }} > {message && {message}} diff --git a/web/apps/photos/src/components/GalleryEmptyState.tsx b/web/apps/photos/src/components/GalleryEmptyState.tsx deleted file mode 100644 index da1dcf1a2c..0000000000 --- a/web/apps/photos/src/components/GalleryEmptyState.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; -import FolderIcon from "@mui/icons-material/FolderOutlined"; -import { Button, Stack, Typography, styled } from "@mui/material"; -import { EnteLogo } from "ente-base/components/EnteLogo"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import uploadManager from "services/upload/uploadManager"; - -export default function GalleryEmptyState({ openUploader }) { - return ( - - - - - }} - /> - - - {t("welcome_to_ente_subtitle")} - - - - - - - - - - ); -} - -const Wrapper = styled("div")` - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -`; - -/** - * Prevent the image from being selected _and_ dragged, since dragging it - * triggers the our dropdown selector overlay. - */ -const NonDraggableImage = styled("img")` - pointer-events: none; - user-select: none; -`; diff --git a/web/apps/photos/src/components/ItemList.tsx b/web/apps/photos/src/components/ItemList.tsx deleted file mode 100644 index f9648460ea..0000000000 --- a/web/apps/photos/src/components/ItemList.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Box, Tooltip } from "@mui/material"; -import memoize from "memoize-one"; -import React, { ReactElement } from "react"; -import { - FixedSizeList as List, - ListChildComponentProps, - ListItemKeySelector, - areEqual, -} from "react-window"; - -export interface ItemListProps { - items: T[]; - generateItemKey: (item: T) => string | number; - getItemTitle: (item: T) => string; - renderListItem: (item: T) => React.JSX.Element; - maxHeight?: number; - itemSize?: number; -} - -interface ItemData { - renderListItem: (item: T) => React.JSX.Element; - getItemTitle: (item: T) => string; - items: T[]; -} - -const createItemData: ( - renderListItem: (item: T) => React.JSX.Element, - getItemTitle: (item: T) => string, - items: T[], -) => ItemData = memoize((renderListItem, getItemTitle, items) => ({ - renderListItem, - getItemTitle, - items, -})); - -// @ts-expect-error "TODO(MR): Understand and fix the type error here" -const Row: ({ - index, - style, - data, -}: ListChildComponentProps>) => ReactElement = React.memo( - ({ index, style, data }) => { - const { renderListItem, items, getItemTitle } = data; - return ( - -
{renderListItem(items[index])}
-
- ); - }, - areEqual, -); - -export default function ItemList(props: ItemListProps) { - const itemData = createItemData( - props.renderListItem, - props.getItemTitle, - props.items, - ); - - const getItemKey: ListItemKeySelector> = (index, data) => { - const { items } = data; - return props.generateItemKey(items[index]); - }; - - return ( - - - {Row} - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar.tsx b/web/apps/photos/src/components/Sidebar.tsx index b7d5ac9a81..6af8de34da 100644 --- a/web/apps/photos/src/components/Sidebar.tsx +++ b/web/apps/photos/src/components/Sidebar.tsx @@ -10,7 +10,6 @@ import NorthEastIcon from "@mui/icons-material/NorthEast"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; import { Box, - Button, Dialog, DialogContent, Divider, @@ -31,17 +30,17 @@ import { LinkButton } from "ente-base/components/LinkButton"; import { RowButton, RowButtonDivider, + RowButtonEndActivityIndicator, RowButtonGroup, RowButtonGroupHint, RowSwitch, } from "ente-base/components/RowButton"; import { SpacedRow } from "ente-base/components/containers"; -import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; +import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { - NestedSidebarDrawer, SidebarDrawer, - SidebarDrawerTitlebar, + TitledNestedSidebarDrawer, type NestedSidebarDrawerVisibilityProps, } from "ente-base/components/mui/SidebarDrawer"; import { useIsSmallWidth } from "ente-base/components/utils/hooks"; @@ -61,6 +60,10 @@ import log from "ente-base/log"; import { savedLogs } from "ente-base/log-web"; import { customAPIHost } from "ente-base/origins"; import { downloadString } from "ente-base/utils/web"; +import { + isHLSGenerationSupported, + toggleHLSGeneration, +} from "ente-gallery/services/video"; import { DeleteAccount } from "ente-new/photos/components/DeleteAccount"; import { DropdownInput } from "ente-new/photos/components/DropdownInput"; import { MLSettings } from "ente-new/photos/components/sidebar/MLSettings"; @@ -71,6 +74,7 @@ import { } from "ente-new/photos/components/utils/dialog"; import { downloadAppDialogAttributes } from "ente-new/photos/components/utils/download"; import { + useHLSGenerationStatusSnapshot, useSettingsSnapshot, useUserDetailsSnapshot, } from "ente-new/photos/components/utils/use-snapshot"; @@ -80,6 +84,7 @@ import { TRASH_SECTION, } from "ente-new/photos/services/collection"; import type { CollectionSummaries } from "ente-new/photos/services/collection/ui"; +import exportService from "ente-new/photos/services/export"; import { isMLSupported } from "ente-new/photos/services/ml"; import { isDevBuildAndUser, @@ -106,10 +111,6 @@ import { } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { initiateEmail, openURL } from "ente-new/photos/utils/web"; -import { - FlexWrapper, - VerticallyCentered, -} from "ente-shared/components/Container"; import { t } from "i18next"; import { useRouter } from "next/router"; import { GalleryContext } from "pages/gallery"; @@ -123,7 +124,6 @@ import React, { } from "react"; import { Trans } from "react-i18next"; import { getUncategorizedCollection } from "services/collectionService"; -import exportService from "services/export"; import { testUpload } from "../../tests/upload.test"; import { SubscriptionCard } from "./SubscriptionCard"; @@ -211,13 +211,10 @@ const UserDetailsSection: React.FC = ({ onShowPlanSelector, }) => { const userDetails = useUserDetailsSnapshot(); - const [memberSubscriptionManageView, setMemberSubscriptionManageView] = - useState(false); - - const openMemberSubscriptionManage = () => - setMemberSubscriptionManageView(true); - const closeMemberSubscriptionManage = () => - setMemberSubscriptionManageView(false); + const { + show: showManageMemberSubscription, + props: manageMemberSubscriptionVisibilityProps, + } = useModalVisibility(); useEffect(() => { if (sidebarOpen) void syncUserDetails(); @@ -233,7 +230,7 @@ const UserDetailsSection: React.FC = ({ const handleSubscriptionCardClick = () => { if (isNonAdminFamilyMember) { - openMemberSubscriptionManage(); + showManageMemberSubscription(); } else { if ( userDetails && @@ -268,11 +265,10 @@ const UserDetailsSection: React.FC = ({ /> )}
- {isNonAdminFamilyMember && ( - )} @@ -372,7 +368,15 @@ const SubscriptionStatus: React.FC = ({ ); }; -function MemberSubscriptionManage({ open, userDetails, onClose }) { +type ManageMemberSubscriptionProps = ModalVisibilityProps & { + userDetails: UserDetails; +}; + +const ManageMemberSubscription: React.FC = ({ + open, + onClose, + userDetails, +}) => { const { showMiniDialog } = useBaseContext(); const fullScreen = useIsSmallWidth(); @@ -387,10 +391,6 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { }, }); - if (!userDetails) { - return <>; - } - return ( @@ -403,7 +403,7 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { - + {t("subscription_info_family")} @@ -412,27 +412,24 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { {familyAdminEmail(userDetails) ?? ""} - - - - - + + {t("leave_family_plan")} + + ); -} +}; type ShortcutSectionProps = SectionProps & { collectionSummaries: SidebarProps["collectionSummaries"]; @@ -582,7 +579,7 @@ const UtilitySection: React.FC = ({ label={t("export_data")} endIcon={ exportService.isExportInProgress() && ( - + ) } onClick={handleExport} @@ -658,6 +655,7 @@ const InfoSection: React.FC = () => { type AccountProps = NestedSidebarDrawerVisibilityProps & Pick; + const Account: React.FC = ({ open, onClose, @@ -689,55 +687,49 @@ const Account: React.FC = ({ }; return ( - - - - - - - } - label={t("recovery_key")} - onClick={showRecoveryKey} - /> - - - - - - - - - - - - - - - + + + + + } + label={t("recovery_key")} + onClick={showRecoveryKey} + /> + + + + + + + + + + + + + + = ({ {...deleteAccountVisibilityProps} {...{ onAuthenticateUser }} /> - + ); }; @@ -769,6 +761,9 @@ const Preferences: React.FC = ({ const { show: showMLSettings, props: mlSettingsVisibilityProps } = useModalVisibility(); + const hlsGenStatusSnapshot = useHLSGenerationStatusSnapshot(); + const isHLSGenerationEnabled = !!hlsGenStatusSnapshot?.enabled; + useEffect(() => { if (open) void syncSettings(); }, [open]); @@ -779,37 +774,43 @@ const Preferences: React.FC = ({ }; return ( - - - + + + + + {isMLSupported && ( + + } + label={t("ml_search")} + onClick={showMLSettings} + /> + + )} + } + label={t("map")} + onClick={showMapSettings} /> - - - - - {isMLSupported && ( - - } - label={t("ml_search")} - onClick={showMLSettings} - /> - - )} - } - label={t("map")} - onClick={showMapSettings} - /> - } - label={t("advanced")} - onClick={showAdvancedSettings} - /> - + } + label={t("advanced")} + onClick={showAdvancedSettings} + /> + {isHLSGenerationSupported && ( + + void toggleHLSGeneration()} + /> + + )} = ({ {...mlSettingsVisibilityProps} onRootClose={handleRootClose} /> - + ); }; @@ -899,6 +900,8 @@ const localeName = (locale: SupportedLocale) => { return "日本語"; case "ar-SA": return "اَلْعَرَبِيَّةُ"; + case "tr-TR": + return "Türkçe"; } }; @@ -955,28 +958,21 @@ const MapSettings: React.FC = ({ }; return ( - - - - - - - - - + + + + - + ); }; @@ -1013,41 +1009,35 @@ const AdvancedSettings: React.FC = ({ void electron?.toggleAutoLaunch().then(refreshAutoLaunchEnabled); return ( - - - - - - - - - - {t("faster_upload_description")} - - - {electron && ( - - - - )} + + + + + + + {t("faster_upload_description")} + + {electron && ( + + + + )} - + ); }; @@ -1087,71 +1077,68 @@ const Help: React.FC = ({ }; return ( - - - + + + } + label={t("ente_help")} + onClick={handleHelp} + /> + + + } + label={t("blog")} + onClick={handleBlog} + /> + + } + label={t("request_feature")} + onClick={handleRequestFeature} + /> + + + } + label={ + + + {t("support")} + + + } + onClick={handleSupport} + /> + + + + + {t("view_logs")} + + } + onClick={confirmViewLogs} /> - - - } - label={t("ente_help")} - onClick={handleHelp} - /> - - - } - label={t("blog")} - onClick={handleBlog} - /> - - } - label={t("request_feature")} - onClick={handleRequestFeature} - /> - - - } - label={ - - - {t("support")} - - - } - onClick={handleSupport} - /> - - - + {isDevBuildAndUser() && ( - {t("view_logs")} + {ut("Test upload")} } - onClick={confirmViewLogs} + onClick={testUpload} /> - {isDevBuildAndUser() && ( - - {ut("Test upload")} - - } - onClick={testUpload} - /> - )} - + )} - + ); }; diff --git a/web/apps/photos/src/components/Upload.tsx b/web/apps/photos/src/components/Upload.tsx index e3a5bdc3b9..58dfbb4073 100644 --- a/web/apps/photos/src/components/Upload.tsx +++ b/web/apps/photos/src/components/Upload.tsx @@ -18,6 +18,7 @@ import { SpacedRow } from "ente-base/components/containers"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import { RowButton } from "ente-base/components/RowButton"; +import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { useIsTouchscreen } from "ente-base/components/utils/hooks"; import { useModalVisibility, @@ -27,12 +28,17 @@ import { useBaseContext } from "ente-base/context"; import { basename, dirname, joinPath } from "ente-base/file-name"; import log from "ente-base/log"; import type { CollectionMapping, Electron, ZipItem } from "ente-base/types/ipc"; +import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import { useFileInput } from "ente-gallery/components/utils/use-file-input"; -import type { - FileAndPath, - UploadItem, - UploadPhase, +import { + groupItemsBasedOnParentFolder, + uploadPathPrefix, + type FileAndPath, + type UploadItem, + type UploadItemAndPath, + type UploadPhase, } from "ente-gallery/services/upload"; +import type { ParsedMetadataJSON } from "ente-gallery/services/upload/metadata-json"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { UploaderNameInput } from "ente-new/albums/components/UploaderNameInput"; @@ -40,7 +46,6 @@ import { CollectionMappingChoice } from "ente-new/photos/components/CollectionMa import type { CollectionSelectorAttributes } from "ente-new/photos/components/CollectionSelector"; import { downloadAppDialogAttributes } from "ente-new/photos/components/utils/download"; import { getLatestCollections } from "ente-new/photos/services/collections"; -import { exportMetadataDirectoryName } from "ente-new/photos/services/export"; import { redirectToCustomerPortal } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { CustomError } from "ente-shared/error"; @@ -65,17 +70,14 @@ import type { UploadCounter, UploadFileNames, UploadItemWithCollection, -} from "services/upload/uploadManager"; -import uploadManager from "services/upload/uploadManager"; +} from "services/upload-manager"; +import { uploadManager } from "services/upload-manager"; import watcher from "services/watch"; import { SetLoading } from "types/gallery"; import { getOrCreateAlbum } from "utils/collection"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; -import { SetCollectionNamerAttributes } from "./Collections/CollectionNamer"; import { UploadProgress } from "./UploadProgress"; -export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; - interface UploadProps { syncWithRemote: (force?: boolean, silent?: boolean) => Promise; closeUploadTypeSelector: () => void; @@ -89,7 +91,6 @@ interface UploadProps { * Close the collection selector if it is open. */ onCloseCollectionSelector?: () => void; - setCollectionNamerAttributes?: SetCollectionNamerAttributes; setLoading: SetLoading; setShouldDisableDropzone: (value: boolean) => void; showCollectionSelector?: () => void; @@ -118,8 +119,6 @@ interface UploadProps { type UploadType = "files" | "folders" | "zips"; -type UploadItemAndPath = [UploadItem, string]; - /** * Top level component that houses the infrastructure for handling uploads. */ @@ -152,12 +151,17 @@ export const Upload: React.FC = ({ useState(new Map()); const [percentComplete, setPercentComplete] = useState(0); const [hasLivePhotos, setHasLivePhotos] = useState(false); + const [prefilledNewAlbumName, setPrefilledNewAlbumName] = useState(""); const [openCollectionMappingChoice, setOpenCollectionMappingChoice] = useState(false); const [importSuggestion, setImportSuggestion] = useState( defaultImportSuggestion, ); + const { + show: showNewAlbumNameInput, + props: newAlbumNameInputVisibilityProps, + } = useModalVisibility(); const { show: showUploaderNameInput, props: uploaderNameInputVisibilityProps, @@ -234,7 +238,7 @@ export const Upload: React.FC = ({ const currentUploadPromise = useRef | undefined>(undefined); const uploadRunning = useRef(false); - const uploaderNameRef = useRef(null); + const uploaderNameRef = useRef(""); const isDragAndDrop = useRef(false); /** @@ -490,7 +494,7 @@ export const Upload: React.FC = ({ publicCollectionGalleryContext.credentials.accessToken, ), ); - uploaderNameRef.current = uploaderName; + uploaderNameRef.current = uploaderName ?? ""; showUploaderNameInput(); return; } @@ -535,8 +539,10 @@ export const Upload: React.FC = ({ if (importSuggestion.hasNestedFolders) { showNextModal = () => setOpenCollectionMappingChoice(true); } else { - showNextModal = () => - showCollectionCreateModal(importSuggestion.rootFolderName); + showNextModal = () => { + setPrefilledNewAlbumName(importSuggestion.rootFolderName); + showNewAlbumNameInput(); + }; } props.onOpenCollectionSelector({ @@ -550,7 +556,7 @@ export const Upload: React.FC = ({ const preCollectionCreationAction = async () => { props.onCloseCollectionSelector?.(); - props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload()); + props.setShouldDisableDropzone(uploadManager.isUploadInProgress()); setUploadPhase("preparing"); setUploadProgressView(true); }; @@ -561,8 +567,9 @@ export const Upload: React.FC = ({ ) => { await preCollectionCreationAction(); const uploadItemsWithCollection = uploadItemsAndPaths.current.map( - ([uploadItem], index) => ({ + ([uploadItem, path], index) => ({ uploadItem, + pathPrefix: uploadPathPrefix(path), localID: index, collectionID: collection.id, }), @@ -582,14 +589,17 @@ export const Upload: React.FC = ({ await preCollectionCreationAction(); let uploadItemsWithCollection: UploadItemWithCollection[] = []; const collections: Collection[] = []; - let collectionNameToUploadItems = new Map(); + let collectionNameToUploadItems = new Map< + string, + UploadItemAndPath[] + >(); if (mapping == "root") { collectionNameToUploadItems.set( collectionName, - uploadItemsAndPaths.current.map(([i]) => i), + uploadItemsAndPaths.current, ); } else { - collectionNameToUploadItems = groupFilesBasedOnParentFolder( + collectionNameToUploadItems = groupItemsBasedOnParentFolder( uploadItemsAndPaths.current, collectionName, ); @@ -609,8 +619,9 @@ export const Upload: React.FC = ({ props.setCollections([...existingCollections, ...collections]); uploadItemsWithCollection = [ ...uploadItemsWithCollection, - ...uploadItems.map((uploadItem) => ({ + ...uploadItems.map(([uploadItem, path]) => ({ localID: index++, + pathPrefix: uploadPathPrefix(path), collectionID: collection.id, uploadItem, })), @@ -642,8 +653,10 @@ export const Upload: React.FC = ({ await currentUploadPromise.current; }; - const preUploadAction = async () => { - uploadManager.prepareForNewUpload(); + const preUploadAction = async ( + parsedMetadataJSONMap?: Map, + ) => { + uploadManager.prepareForNewUpload(parsedMetadataJSONMap); setUploadProgressView(true); await props.syncWithRemote(true, true); }; @@ -682,10 +695,7 @@ export const Upload: React.FC = ({ if (!wereFilesProcessed) closeUploadProgress(); if (isDesktop) { if (watcher.isUploadRunning()) { - await watcher.allFileUploadsDone( - uploadItemsWithCollection, - collections, - ); + await watcher.allFileUploadsDone(uploadItemsWithCollection); } else if (watcher.isSyncPaused()) { // Resume folder watch after the user upload that // interrupted it is done. @@ -704,10 +714,10 @@ export const Upload: React.FC = ({ const retryFailed = async () => { try { log.info("Retrying failed uploads"); - const { items, collections } = - uploadManager.getFailedItemsWithCollections(); + const { items, collections, parsedMetadataJSONMap } = + uploadManager.failedItemState(); const uploaderName = uploadManager.getUploaderName(); - await preUploadAction(); + await preUploadAction(parsedMetadataJSONMap); await uploadManager.uploadItems(items, collections, uploaderName); } catch (e) { log.error("Retrying failed uploads failed", e); @@ -754,15 +764,6 @@ export const Upload: React.FC = ({ uploadFilesToNewCollections("root", collectionName); }; - const showCollectionCreateModal = (suggestedName: string) => { - props.setCollectionNamerAttributes({ - title: t("new_album"), - buttonText: t("create"), - autoFilledName: suggestedName, - callback: uploadToSingleNewCollection, - }); - }; - const cancelUploads = () => { uploadManager.cancelRunningUpload(); }; @@ -787,26 +788,18 @@ export const Upload: React.FC = ({ } }; - const handlePublicUpload = async ( - uploaderName: string, - skipSave?: boolean, - ) => { - try { - if (!skipSave) { - savePublicCollectionUploaderName( - getPublicCollectionUID( - publicCollectionGalleryContext.credentials.accessToken, - ), - uploaderName, - ); - } - await uploadFilesToExistingCollection( - props.uploadCollection, - uploaderName, - ); - } catch (e) { - log.error("public upload failed ", e); - } + const handlePublicUpload = async (uploaderName: string) => { + savePublicCollectionUploaderName( + getPublicCollectionUID( + publicCollectionGalleryContext.credentials.accessToken, + ), + uploaderName, + ); + + await uploadFilesToExistingCollection( + props.uploadCollection, + uploaderName, + ); }; const handleCollectionMappingSelect = (mapping: CollectionMapping) => @@ -852,6 +845,14 @@ export const Upload: React.FC = ({ finishedUploads={finishedUploads} cancelUploads={cancelUploads} /> + [j], - * b => [e,f,g], - * c => [h, i] - * ] - * - * @param defaultFolderName Optional collection name to use for any rooted files - * that do not have a parent folder. The function will throw if a default is not - * provided and we encounter any such files without a parent. - */ -const groupFilesBasedOnParentFolder = ( - uploadItemAndPaths: UploadItemAndPath[], - defaultFolderName: string | undefined, -) => { - const result = new Map(); - for (const [uploadItem, pathOrName] of uploadItemAndPaths) { - let folderPath = pathOrName.substring(0, pathOrName.lastIndexOf("/")); - // If the parent folder of a file is "metadata", then we consider it to - // be part of the parent folder. - // - // e.g. for FileList - // - // [a/x.png, a/metadata/x.png.json] - // - // they will both be grouped into the collection "a". This is so that we - // cluster the metadata json files in the same collection as the file it - // is for. - if (folderPath.endsWith(exportMetadataDirectoryName)) { - folderPath = folderPath.substring(0, folderPath.lastIndexOf("/")); - } - let folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1); - if (!folderName) { - if (!defaultFolderName) - throw Error(`Leaf file (without default): ${folderPath}`); - folderName = defaultFolderName; - } - if (!result.has(folderName)) result.set(folderName, []); - result.get(folderName).push(uploadItem); - } - return result; -}; - const setPendingUploads = async ( electron: Electron, collections: Collection[], diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index 71fba1592c..4af28ab8e0 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -18,28 +18,39 @@ import { Snackbar, Stack, styled, + Tooltip, Typography, type AccordionProps, type DialogProps, } from "@mui/material"; -import ItemList from "components/ItemList"; +import { SpacedRow } from "ente-base/components/containers"; import { FilledIconButton } from "ente-base/components/mui"; import { useBaseContext } from "ente-base/context"; import { formattedListJoin } from "ente-base/i18n"; -import { - type UploadPhase, - type UploadResult, -} from "ente-gallery/services/upload"; -import { SpaceBetweenFlex } from "ente-shared/components/Container"; +import { type UploadPhase } from "ente-gallery/services/upload"; import { t } from "i18next"; -import React, { createContext, useContext, useEffect, useState } from "react"; +import memoize from "memoize-one"; +import React, { + createContext, + ReactElement, + useContext, + useEffect, + useState, +} from "react"; import { Trans } from "react-i18next"; +import { + areEqual, + FixedSizeList as List, + ListChildComponentProps, + ListItemKeySelector, +} from "react-window"; import type { + FinishedUploadResult, InProgressUpload, SegregatedFinishedUploads, UploadCounter, UploadFileNames, -} from "services/upload/uploadManager"; +} from "services/upload-manager"; interface UploadProgressProps { open: boolean; @@ -172,22 +183,20 @@ const UploadProgressTitle: React.FC = () => { return ( - + {t("file_upload")} - - - - {expanded ? : } - - - - - - - + + + {expanded ? : } + + + + + + ); }; @@ -251,7 +260,6 @@ const uploadedFileCount = ( let c = 0; c += finishedUploads.get("uploaded")?.length ?? 0; c += finishedUploads.get("uploadedWithStaticThumbnail")?.length ?? 0; - c += finishedUploads.get("addedSymlink")?.length ?? 0; return c; }; @@ -474,7 +482,7 @@ const NotUploadSectionHeader = styled("div")( ); interface ResultSectionProps { - uploadResult: UploadResult; + uploadResult: FinishedUploadResult; sectionTitle: string; sectionInfo?: React.ReactNode; } @@ -561,6 +569,93 @@ const TitleText: React.FC = ({ title, count }) => ( ); +interface ItemListProps { + items: T[]; + generateItemKey: (item: T) => string | number; + getItemTitle: (item: T) => string; + renderListItem: (item: T) => React.JSX.Element; + maxHeight?: number; + itemSize?: number; +} + +interface ItemData { + renderListItem: (item: T) => React.JSX.Element; + getItemTitle: (item: T) => string; + items: T[]; +} + +const createItemData: ( + renderListItem: (item: T) => React.JSX.Element, + getItemTitle: (item: T) => string, + items: T[], +) => ItemData = memoize((renderListItem, getItemTitle, items) => ({ + renderListItem, + getItemTitle, + items, +})); + +// @ts-expect-error "TODO: Understand and fix the type error here" +const Row: ({ + index, + style, + data, +}: ListChildComponentProps>) => ReactElement = React.memo( + ({ index, style, data }) => { + const { renderListItem, items, getItemTitle } = data; + return ( + +
{renderListItem(items[index])}
+
+ ); + }, + areEqual, +); + +function ItemList(props: ItemListProps) { + const itemData = createItemData( + props.renderListItem, + props.getItemTitle, + props.items, + ); + + const getItemKey: ListItemKeySelector> = (index, data) => { + const { items } = data; + return props.generateItemKey(items[index]); + }; + + return ( + + + {Row} + + + ); +} + const DoneFooter: React.FC = () => { const { uploadPhase, finishedUploads, retryFailed, onClose } = useContext( UploadProgressContext, diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 57a038bd85..c88e332776 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -177,18 +177,18 @@ const NoWatches: React.FC = () => ( {t("watch_folders_hint_1")} - - - + + + {t("watch_folders_hint_2")} - - - - - + + + + + {t("watch_folders_hint_3")} - - + + ); diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index f17ce008ee..5824cb1890 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -3,6 +3,7 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import { CssBaseline, Typography } from "@mui/material"; import { styled, ThemeProvider } from "@mui/material/styles"; import { useNotification } from "components/utils/hooks-app"; +import type { User } from "ente-accounts/services/user"; import { clientPackageName, isDesktop, staticAppTitle } from "ente-base/app"; import { CenteredRow } from "ente-base/components/containers"; import { CustomHead } from "ente-base/components/Head"; @@ -22,6 +23,10 @@ import { BaseContext, deriveBaseContext } from "ente-base/context"; import log from "ente-base/log"; import { logStartupBanner } from "ente-base/log-web"; import { AppUpdate } from "ente-base/types/ipc"; +import { + initVideoProcessing, + isHLSGenerationSupported, +} from "ente-gallery/services/video"; import { Notification } from "ente-new/photos/components/Notification"; import { ThemedLoadingBar } from "ente-new/photos/components/ThemedLoadingBar"; import { @@ -29,6 +34,7 @@ import { updateReadyToInstallDialogAttributes, } from "ente-new/photos/components/utils/download"; import { useLoadingBar } from "ente-new/photos/components/utils/use-loading-bar"; +import { resumeExportsIfNeeded } from "ente-new/photos/services/export"; import { runMigrations } from "ente-new/photos/services/migration"; import { initML, isMLSupported } from "ente-new/photos/services/ml"; import { getFamilyPortalRedirectURL } from "ente-new/photos/services/user-details"; @@ -38,12 +44,10 @@ import { getData, isLocalStorageAndIndexedDBMismatch, } from "ente-shared/storage/localStorage"; -import type { User } from "ente-shared/user/types"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { resumeExportsIfNeeded } from "services/export"; import { photosLogout } from "services/logout"; import "photoswipe/dist/photoswipe.css"; @@ -108,6 +112,7 @@ const App: React.FC = ({ Component, pageProps }) => { }; if (isMLSupported) initML(); + if (isHLSGenerationSupported) void initVideoProcessing(); electron.onOpenEnteURL(handleOpenEnteURL); electron.onAppUpdateAvailable(showUpdateDialog); diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index f5f7d8d620..46b151ef22 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -3,11 +3,7 @@ import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; import MenuIcon from "@mui/icons-material/Menu"; import { IconButton, Stack, Typography } from "@mui/material"; import { AuthenticateUser } from "components/AuthenticateUser"; -import CollectionNamer, { - CollectionNamerAttributes, -} from "components/Collections/CollectionNamer"; import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader"; -import { Export } from "components/Export"; import { TimeStampListItem } from "components/FileList"; import { FileListWithViewer } from "components/FileListWithViewer"; import { @@ -15,14 +11,15 @@ import { FilesDownloadProgressAttributes, } from "components/FilesDownloadProgress"; import { FixCreationTime } from "components/FixCreationTime"; -import GalleryEmptyState from "components/GalleryEmptyState"; import { Sidebar } from "components/Sidebar"; -import { Upload, type UploadTypeSelectorIntent } from "components/Upload"; +import { Upload } from "components/Upload"; import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog"; import { stashRedirect } from "ente-accounts/services/redirect"; +import { isSessionInvalid } from "ente-accounts/services/session"; import type { MiniDialogAttributes } from "ente-base/components/MiniDialog"; import { NavbarBase } from "ente-base/components/Navbar"; +import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { CenteredRow } from "ente-base/components/containers"; import { TranslucentLoadingOverlay } from "ente-base/components/loaders"; import type { ButtonishProps } from "ente-base/components/mui"; @@ -32,7 +29,13 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import log from "ente-base/log"; +import { + clearSessionStorage, + haveCredentialsInSession, + masterKeyFromSession, +} from "ente-base/session"; import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; +import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import { type Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { @@ -43,6 +46,7 @@ import { CollectionSelector, type CollectionSelectorAttributes, } from "ente-new/photos/components/CollectionSelector"; +import { Export } from "ente-new/photos/components/Export"; import { PlanSelector } from "ente-new/photos/components/PlanSelector"; import { SearchBar, @@ -50,12 +54,14 @@ import { } from "ente-new/photos/components/SearchBar"; import { WhatsNew } from "ente-new/photos/components/WhatsNew"; import { + GalleryEmptyState, PeopleEmptyState, SearchResultsHeader, } from "ente-new/photos/components/gallery"; import { constructUserIDToEmailMap, createShareeSuggestionEmails, + validateKey, } from "ente-new/photos/components/gallery/helpers"; import { useGalleryReducer, @@ -70,6 +76,7 @@ import { } from "ente-new/photos/services/collection"; import { areOnlySystemCollections } from "ente-new/photos/services/collection/ui"; import { getAllLocalCollections } from "ente-new/photos/services/collections"; +import exportService from "ente-new/photos/services/export"; import { getLocalFiles, getLocalTrashedFiles, @@ -92,9 +99,6 @@ import { verifyStripeSubscription, } from "ente-new/photos/services/user-details"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { FlexWrapper } from "ente-shared/components/Container"; -import { getRecoveryKey } from "ente-shared/crypto/helpers"; -import { CustomError } from "ente-shared/error"; import { getData } from "ente-shared/storage/localStorage"; import { getToken, @@ -103,7 +107,6 @@ import { setIsFirstLogin, setJustSignedUp, } from "ente-shared/storage/localStorage/helpers"; -import { clearKeys, getKey } from "ente-shared/storage/sessionStorage"; import { t } from "i18next"; import { useRouter, type NextRouter } from "next/router"; import { createContext, useCallback, useEffect, useRef, useState } from "react"; @@ -115,9 +118,7 @@ import { createUnCategorizedCollection, removeFromFavorites, } from "services/collectionService"; -import exportService from "services/export"; -import uploadManager from "services/upload/uploadManager"; -import { isTokenValid } from "services/userService"; +import { uploadManager } from "services/upload-manager"; import { GalleryContextType, SelectedState, @@ -131,6 +132,17 @@ import { } from "utils/collection"; import { getSelectedFiles, handleFileOp, type FileOp } from "utils/file"; +/** + * Options to customize the behaviour of the sync with remote that gets + * triggered on various actions within the gallery and its descendants. + */ +interface SyncWithRemoteOpts { + /** Force a sync to happen (default: no) */ + force?: boolean; + /** Perform the sync without showing a global loading bar (default: no) */ + silent?: boolean; +} + const defaultGalleryContext: GalleryContextType = { setActiveCollectionID: () => null, syncWithRemote: () => null, @@ -178,22 +190,17 @@ const Page: React.FC = () => { context: { mode: "albums", collectionID: ALL_SECTION }, }); const [blockingLoad, setBlockingLoad] = useState(false); - const [collectionNamerAttributes, setCollectionNamerAttributes] = - useState(null); - const [collectionNamerView, setCollectionNamerView] = useState(false); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); const [dragAndDropFiles, setDragAndDropFiles] = useState( [], ); const [isFileViewerOpen, setIsFileViewerOpen] = useState(false); - const syncInProgress = useRef(false); - const syncInterval = useRef | undefined>( - undefined, - ); - const resync = useRef<{ force: boolean; silent: boolean } | undefined>( - undefined, - ); + /**`true` if a sync is currently in progress. */ + const isSyncing = useRef(false); + /** Set to the {@link SyncWithRemoteOpts} of the last sync that was enqueued + while one was already in progress. */ + const resyncOpts = useRef(undefined); const [userIDToEmailMap, setUserIDToEmailMap] = useState>(null); @@ -222,6 +229,9 @@ const Page: React.FC = () => { filesDownloadProgressAttributesList, setFilesDownloadProgressAttributesList, ] = useState([]); + const [, setPostCreateAlbumOp] = useState( + undefined, + ); const [openCollectionSelector, setOpenCollectionSelector] = useState(false); const [collectionSelectorAttributes, setCollectionSelectorAttributes] = @@ -241,6 +251,8 @@ const Page: React.FC = () => { show: showAuthenticateUser, props: authenticateUserVisibilityProps, } = useModalVisibility(); + const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = + useModalVisibility(); const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined); @@ -286,32 +298,21 @@ const Page: React.FC = () => { const router = useRouter(); - // Ensure that the keys in local storage are not malformed by verifying that - // the recoveryKey can be decrypted with the masterKey. - // Note: This is not bullet-proof. - const validateKey = async () => { - try { - await getRecoveryKey(); - return true; - } catch { - logout(); - return false; - } - }; - useEffect(() => { - const key = getKey("encryptionKey"); const token = getToken(); - if (!key || !token) { + if (!haveCredentialsInSession() || !token) { stashRedirect("/gallery"); router.push("/"); return; } preloadImage("/images/subscription-card-background"); + const electron = globalThis.electron; - const main = async () => { - const valid = await validateKey(); - if (!valid) { + let syncIntervalID: ReturnType | undefined; + + void (async () => { + if (!(await validateKey())) { + logout(); return; } initSettings(); @@ -335,21 +336,23 @@ const Page: React.FC = () => { hiddenFiles: await getLocalFiles("hidden"), trashedFiles: await getLocalTrashedFiles(), }); - await syncWithRemote(true); + await syncWithRemote({ force: true }); setIsFirstLoad(false); setJustSignedUp(false); - syncInterval.current = setInterval( - () => syncWithRemote(false, true), + syncIntervalID = setInterval( + () => syncWithRemote({ silent: true }), 5 * 60 * 1000 /* 5 minutes */, ); if (electron) { - electron.onMainWindowFocus(() => syncWithRemote(false, true)); + electron.onMainWindowFocus(() => + syncWithRemote({ silent: true }), + ); if (await shouldShowWhatsNew(electron)) showWhatsNew(); } - }; - main(); + })(); + return () => { - clearInterval(syncInterval.current); + clearInterval(syncIntervalID); if (electron) electron.onMainWindowFocus(undefined); }; }, []); @@ -371,10 +374,6 @@ const Page: React.FC = () => { ); }, [user, normalCollections, familyData]); - useEffect(() => { - collectionNamerAttributes && setCollectionNamerView(true); - }, [collectionNamerAttributes]); - useEffect(() => { if (typeof activeCollectionID == "undefined" || !router.isReady) { return; @@ -389,7 +388,7 @@ const Page: React.FC = () => { }, [activeCollectionID, router.isReady]); useEffect(() => { - if (router.isReady && getKey("encryptionKey")) { + if (router.isReady && haveCredentialsInSession()) { handleSubscriptionCompletionRedirectIfNeeded( showMiniDialog, showLoadingBar, @@ -451,12 +450,12 @@ const Page: React.FC = () => { // - Any of the modals are open. uploadTypeSelectorView || openCollectionSelector || - collectionNamerView || sidebarVisibilityProps.open || planSelectorVisibilityProps.open || fixCreationTimeVisibilityProps.open || exportVisibilityProps.open || authenticateUserVisibilityProps.open || + albumNameInputVisibilityProps.open || isFileViewerOpen ) { return; @@ -522,7 +521,7 @@ const Page: React.FC = () => { setTimeout(hideLoadingBar, 0); }, [showLoadingBar, hideLoadingBar]); - const handleFileAndCollectionSyncWithRemote = useCallback(async () => { + const fileAndCollectionSyncWithRemote = useCallback(async () => { const didUpdateFiles = await syncCollectionAndFiles({ onSetCollections: ( collections, @@ -551,27 +550,39 @@ const Page: React.FC = () => { } }, []); - const handleSyncWithRemote = useCallback( - async (force = false, silent = false) => { + const syncWithRemote = useCallback( + async (opts?: SyncWithRemoteOpts) => { + const { force, silent } = opts ?? {}; + + // Pre-flight checks. if (!navigator.onLine) return; - if (syncInProgress.current && !force) { - resync.current = { force, silent }; + if (await isSessionInvalid()) { + showSessionExpiredDialog(); return; } - const isForced = syncInProgress.current && force; - syncInProgress.current = true; - try { - const token = getToken(); - if (!token) { + if (!(await masterKeyFromSession())) { + clearSessionStorage(); + router.push("/credentials"); + return; + } + + // Start or enqueue. + let isForced = false; + if (isSyncing.current) { + if (force) { + isForced = true; + } else { + resyncOpts.current = { force, silent }; return; } - const tokenValid = await isTokenValid(token); - if (!tokenValid) { - throw new Error(CustomError.SESSION_EXPIRED); - } - !silent && showLoadingBar(); + } + + // The sync + isSyncing.current = true; + try { + if (!silent) showLoadingBar(); await preCollectionAndFilesSync(); - await handleFileAndCollectionSyncWithRemote(); + await fileAndCollectionSyncWithRemote(); // syncWithRemote is called with the force flag set to true before // doing an upload. So it is possible, say when resuming a pending // upload, that we get two syncWithRemotes happening in parallel. @@ -581,26 +592,17 @@ const Page: React.FC = () => { await postCollectionAndFilesSync(); } } catch (e) { - switch (e.message) { - case CustomError.SESSION_EXPIRED: - showSessionExpiredDialog(); - break; - case CustomError.KEY_MISSING: - clearKeys(); - router.push("/credentials"); - break; - default: - log.error("syncWithRemote failed", e); - } + log.error("syncWithRemote failed", e); } finally { dispatch({ type: "clearUnsyncedState" }); - !silent && hideLoadingBar(); + if (!silent) hideLoadingBar(); } - syncInProgress.current = false; - if (resync.current) { - const { force, silent } = resync.current; - setTimeout(() => handleSyncWithRemote(force, silent), 0); - resync.current = undefined; + isSyncing.current = false; + + const nextOpts = resyncOpts.current; + if (nextOpts) { + resyncOpts.current = undefined; + setTimeout(() => syncWithRemote(nextOpts), 0); } }, [ @@ -608,13 +610,10 @@ const Page: React.FC = () => { hideLoadingBar, router, showSessionExpiredDialog, - handleFileAndCollectionSyncWithRemote, + fileAndCollectionSyncWithRemote, ], ); - // Alias for existing code. - const syncWithRemote = handleSyncWithRemote; - const setupSelectAllKeyBoardShortcutHandler = () => { const handleKeyUp = (e: KeyboardEvent) => { switch (e.key) { @@ -688,7 +687,7 @@ const Page: React.FC = () => { ); } clearSelection(); - await syncWithRemote(false, true); + await syncWithRemote({ silent: true }); } catch (e) { onGenericError(e); } finally { @@ -724,7 +723,7 @@ const Page: React.FC = () => { ); } clearSelection(); - await syncWithRemote(false, true); + await syncWithRemote({ silent: true }); } catch (e) { onGenericError(e); } finally { @@ -732,26 +731,26 @@ const Page: React.FC = () => { } }; - const showCreateCollectionModal = (op: CollectionOp) => { - const callback = async (collectionName: string) => { - try { - showLoadingBar(); - const collection = await createAlbum(collectionName); - await collectionOpsHelper(op)(collection); - } catch (e) { - onGenericError(e); - } finally { - hideLoadingBar(); - } - }; - return () => - setCollectionNamerAttributes({ - title: t("new_album"), - buttonText: t("create"), - autoFilledName: "", - callback, + const handleCreateAlbumForOp = useCallback( + (op: CollectionOp) => { + setPostCreateAlbumOp(op); + return showAlbumNameInput; + }, + [showAlbumNameInput], + ); + + const handleAlbumNameSubmit = useCallback( + async (name: string) => { + const collection = await createAlbum(name); + setPostCreateAlbumOp((postCreateAlbumOp) => { + // collectionOpsHelper does its own progress and error + // reporting, defer to that. + void collectionOpsHelper(postCreateAlbumOp!)(collection); + return undefined; }); - }; + }, + [collectionOpsHelper], + ); const handleSelectSearchOption = ( searchOption: SearchOption | undefined, @@ -781,9 +780,7 @@ const Page: React.FC = () => { }; const openUploader = (intent?: UploadTypeSelectorIntent) => { - if (!uploadManager.shouldAllowNewUpload()) { - return; - } + if (uploadManager.isUploadInProgress()) return; setUploadTypeSelectorView(true); setUploadTypeSelectorIntent(intent ?? "upload"); }; @@ -897,7 +894,8 @@ const Page: React.FC = () => { value={{ ...defaultGalleryContext, setActiveCollectionID: handleSetActiveCollectionID, - syncWithRemote, + syncWithRemote: (force, silent) => + syncWithRemote({ force, silent }), setBlockingLoad, photoListHeader, userIDToEmailMap, @@ -923,11 +921,6 @@ const Page: React.FC = () => { {...planSelectorVisibilityProps} setLoading={(v) => setBlockingLoad(v)} /> - { { activeCollection, activeCollectionID, activePerson, - setCollectionNamerAttributes, setPhotoListHeader, setFilesDownloadProgressAttributesCreator, filesDownloadProgressAttributesList, @@ -1047,7 +1037,9 @@ const Page: React.FC = () => { + syncWithRemote({ force, silent }) + } closeUploadTypeSelector={setUploadTypeSelectorView.bind( null, false, @@ -1055,7 +1047,6 @@ const Page: React.FC = () => { onOpenCollectionSelector={handleOpenCollectionSelector} onCloseCollectionSelector={handleCloseCollectionSelector} setLoading={setBlockingLoad} - setCollectionNamerAttributes={setCollectionNamerAttributes} setShouldDisableDropzone={setShouldDisableDropzone} onUploadFile={(file) => dispatch({ type: "uploadNormalFile", file }) @@ -1087,7 +1078,10 @@ const Page: React.FC = () => { !normalFiles?.length && !hiddenFiles?.length && activeCollectionID === ALL_SECTION ? ( - + ) : !isInSearchMode && !isFirstLoad && state.view.type == "people" && @@ -1131,7 +1125,7 @@ const Page: React.FC = () => { } onMarkTempDeleted={handleMarkTempDeleted} onSetOpenFileViewer={setIsFileViewerOpen} - onSyncWithRemote={handleSyncWithRemote} + onSyncWithRemote={syncWithRemote} onVisualFeedback={handleVisualFeedback} onSelectCollection={handleSelectCollection} onSelectPerson={handleSelectPerson} @@ -1139,12 +1133,19 @@ const Page: React.FC = () => { )} + ); @@ -1208,7 +1209,7 @@ const SidebarButton: React.FC = ({ onClick }) => ( ); const UploadButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); const isSmallWidth = useIsSmallWidth(); const icon = ; @@ -1241,16 +1242,15 @@ const HiddenSectionNavbarContents: React.FC< direction="row" sx={(theme) => ({ gap: "24px", - width: "100%", + flex: 1, + alignItems: "center", background: theme.vars.palette.background.default, })} > - - {t("section_hidden")} - + {t("section_hidden")} ); diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index 1679e6bdd6..3c1e00b40b 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -8,12 +8,13 @@ import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton" import { useBaseContext } from "ente-base/context"; import log from "ente-base/log"; import { albumsAppOrigin, customAPIHost } from "ente-base/origins"; +import { + haveAuthenticatedSession, + updateSessionFromElectronSafeStorageIfNeeded, +} from "ente-base/session"; import { DevSettings } from "ente-new/photos/components/DevSettings"; -import { saveKeyInSessionStore } from "ente-shared/crypto/helpers"; import localForage from "ente-shared/storage/localForage"; import { getData } from "ente-shared/storage/localStorage"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; -import { getKey } from "ente-shared/storage/sessionStorage"; import { t } from "i18next"; import { useRouter } from "next/router"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -56,38 +57,25 @@ const Page: React.FC = () => { search: currentURL.search, hash: hash, }); - await initLocalForage(); + await ensureIndexedDBAccess(); }; const handleNormalRedirect = async () => { const user = getData("user"); - let key = getKey("encryptionKey"); - const electron = globalThis.electron; - if (!key && electron) { - try { - key = await electron.masterKeyB64(); - } catch (e) { - log.error("Failed to read master key from safe storage", e); - } - if (key) { - await saveKeyInSessionStore("encryptionKey", key, true); - } - } - const token = getToken(); - if (key && token) { + await updateSessionFromElectronSafeStorageIfNeeded(); + if (await haveAuthenticatedSession()) { await router.push("/gallery"); } else if (user?.email) { await router.push("/verify"); } - await initLocalForage(); - setLoading(false); + await ensureIndexedDBAccess(); }; - const initLocalForage = async () => { + const ensureIndexedDBAccess = useCallback(async () => { try { await localForage.ready(); } catch (e) { - log.error("Local storage is not accessible", e); + log.error("IndexDB is not accessible", e); showMiniDialog({ title: t("error"), message: t("local_storage_not_accessible"), @@ -97,13 +85,7 @@ const Page: React.FC = () => { } finally { setLoading(false); } - }; - - const signUp = () => setShowLogin(false); - const login = () => setShowLogin(true); - - const redirectToSignupPage = () => router.push("/signup"); - const redirectToLoginPage = () => router.push("/login"); + }, [showMiniDialog]); return ( @@ -120,11 +102,13 @@ const Page: React.FC = () => { router.push("/signup")} > {t("new_to_ente")} - + router.push("/login")} + > {t("existing_user")} @@ -141,11 +125,13 @@ const Page: React.FC = () => { {showLogin ? ( setShowLogin(false)} /> ) : ( setShowLogin(true)} /> )} diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 4e9a2447eb..25e3d62684 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -15,7 +15,11 @@ import { AccountsPageContents, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; -import { SpacedRow, Stack100vhCenter } from "ente-base/components/containers"; +import { + CenteredFill, + SpacedRow, + Stack100vhCenter, +} from "ente-base/components/containers"; import { EnteLogo } from "ente-base/components/EnteLogo"; import { LoadingIndicator, @@ -28,6 +32,10 @@ import { OverflowMenu, OverflowMenuOption, } from "ente-base/components/OverflowMenu"; +import { + SingleInputForm, + type SingleInputFormProps, +} from "ente-base/components/SingleInputForm"; import { useIsSmallWidth, useIsTouchscreen, @@ -52,10 +60,6 @@ import { } from "ente-new/photos/services/collection"; import { sortFiles } from "ente-new/photos/services/files"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { CenteredFlex } from "ente-shared/components/Container"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import { t } from "i18next"; import { useRouter } from "next/router"; @@ -73,7 +77,7 @@ import { savePublicCollectionPassword, syncPublicFiles, } from "services/publicCollectionService"; -import uploadManager from "services/upload/uploadManager"; +import { uploadManager } from "services/upload-manager"; import { SelectedState, SetFilesDownloadProgressAttributes, @@ -275,9 +279,9 @@ export default function PublicCollectionGallery() { onAddPhotos ? { item: ( - + - + ), height: 104, } @@ -285,7 +289,7 @@ export default function PublicCollectionGallery() { ); }, [onAddPhotos]); - const handleSyncWithRemote = useCallback(async () => { + const syncWithRemote = useCallback(async () => { const collectionUID = getPublicCollectionUID( credentials.current.accessToken, ); @@ -367,16 +371,13 @@ export default function PublicCollectionGallery() { } }, [showLoadingBar, hideLoadingBar]); - // TODO: See gallery - const syncWithRemote = handleSyncWithRemote; - // See: [Note: Visual feedback to acknowledge user actions] const handleVisualFeedback = useCallback(() => { showLoadingBar(); setTimeout(hideLoadingBar, 0); }, [showLoadingBar, hideLoadingBar]); - const verifyLinkPassword: SingleInputFormProps["callback"] = async ( + const handleSubmitPassword: SingleInputFormProps["onSubmit"] = async ( password, setFieldError, ) => { @@ -396,10 +397,9 @@ export default function PublicCollectionGallery() { log.error("Failed to verifyLinkPassword", e); if (isHTTP401Error(e)) { setFieldError(t("incorrect_password")); - } else { - setFieldError(t("generic_error_retry")); + return; } - return; + throw e; } await syncWithRemote(); @@ -456,10 +456,11 @@ export default function PublicCollectionGallery() { {t("link_password_description")} @@ -523,7 +524,7 @@ export default function PublicCollectionGallery() { setFilesDownloadProgressAttributesCreator={ setFilesDownloadProgressAttributesCreator } - onSyncWithRemote={handleSyncWithRemote} + onSyncWithRemote={syncWithRemote} onVisualFeedback={handleVisualFeedback} /> {blockingLoad && } @@ -558,7 +559,7 @@ const EnteLogoLink = styled("a")(({ theme }) => ({ })); const AddPhotosButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); const isSmallWidth = useIsSmallWidth(); const icon = ; @@ -585,7 +586,7 @@ const AddPhotosButton: React.FC = ({ onClick }) => { * shrink on mobile sized screens. */ const AddMorePhotosButton: React.FC = ({ onClick }) => { - const disabled = !uploadManager.shouldAllowNewUpload(); + const disabled = uploadManager.isUploadInProgress(); return ( { - return createCollection(albumName, "album"); -}; +export const createAlbum = (albumName: string) => + createCollection(albumName, "album"); + +// TODO(C2): +const enableC2 = () => isDevBuild && process.env.NEXT_PUBLIC_ENTE_WIP_NEWIMPL; const createCollection = async ( collectionName: string, type: CollectionType, magicMetadataProps?: CollectionMagicMetadataProps, +): Promise => { + if (enableC2()) { + const z = createCollection2(collectionName, type, magicMetadataProps); + z.then((x) => console.log(x)); + return z; + } + return createCollection1(collectionName, type, magicMetadataProps); +}; + +const createCollection1 = async ( + collectionName: string, + type: CollectionType, + magicMetadataProps?: CollectionMagicMetadataProps, ): Promise => { try { const cryptoWorker = await sharedCryptoWorker(); - const encryptionKey = await getActualKey(); + const masterKey = await ensureMasterKeyFromSession(); const token = getToken(); - const collectionKey = await cryptoWorker.generateEncryptionKey(); + const collectionKey = await cryptoWorker.generateKey(); const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } = - await cryptoWorker.encryptToB64(collectionKey, encryptionKey); + await cryptoWorker.encryptBox(collectionKey, masterKey); const { encryptedData: encryptedName, nonce: nameDecryptionNonce } = - await cryptoWorker.encryptUTF8(collectionName, collectionKey); + await cryptoWorker.encryptBox( + new TextEncoder().encode(collectionName), + collectionKey, + ); + let encryptedMagicMetadata: EncryptedMagicMetadata; if (magicMetadataProps) { const magicMetadata = await updateMagicMetadata(magicMetadataProps); - const encryptedMagicMetadataProps = - await cryptoWorker.encryptMetadataJSON({ - jsonValue: magicMetadataProps, - keyB64: collectionKey, - }); - + const { encryptedData, decryptionHeader } = + await cryptoWorker.encryptMetadataJSON( + magicMetadataProps, + collectionKey, + ); encryptedMagicMetadata = { ...magicMetadata, - data: encryptedMagicMetadataProps.encryptedDataB64, - header: encryptedMagicMetadataProps.decryptionHeaderB64, + data: encryptedData, + header: decryptionHeader, }; } const newCollection: EncryptedCollection = { @@ -108,7 +120,7 @@ const createCollection = async ( const createdCollection = await postCollection(newCollection, token); const decryptedCreatedCollection = await getCollectionWithSecrets( createdCollection, - encryptionKey, + masterKey, ); return decryptedCreatedCollection; } catch (e) { @@ -134,9 +146,8 @@ const postCollection = async ( } }; -export const createFavoritesCollection = () => { - return createCollection(FAVORITE_COLLECTION_NAME, "favorites"); -}; +export const createFavoritesCollection = () => + createCollection(favoritesCollectionName, "favorites"); export const addToFavorites = async ( file: EnteFile, @@ -331,279 +342,20 @@ export const deleteCollection = async ( } }; -export const leaveSharedAlbum = async (collectionID: number) => { - try { - const token = getToken(); - - await HTTPService.post( - await apiURL(`/collections/leave/${collectionID}`), - null, - null, - { "X-Auth-Token": token }, - ); - } catch (e) { - log.error("leave shared album failed ", e); - throw e; - } -}; - -export const updateCollectionMagicMetadata = async ( - collection: Collection, - updatedMagicMetadata: CollectionMagicMetadata, -) => { - const token = getToken(); - if (!token) { - return; - } - - const { encryptedDataB64, decryptionHeaderB64 } = await encryptMetadataJSON( - { jsonValue: updatedMagicMetadata.data, keyB64: collection.key }, - ); - - const reqBody: UpdateMagicMetadataRequest = { - id: collection.id, - magicMetadata: { - version: updatedMagicMetadata.version, - count: updatedMagicMetadata.count, - data: encryptedDataB64, - header: decryptionHeaderB64, - }, - }; - - await HTTPService.put( - await apiURL("/collections/magic-metadata"), - reqBody, - null, - { "X-Auth-Token": token }, - ); - const updatedCollection: Collection = { - ...collection, - magicMetadata: { - ...updatedMagicMetadata, - version: updatedMagicMetadata.version + 1, - }, - }; - return updatedCollection; -}; - -export const updateSharedCollectionMagicMetadata = async ( - collection: Collection, - updatedMagicMetadata: CollectionMagicMetadata, -) => { - const token = getToken(); - if (!token) { - return; - } - - const { encryptedDataB64, decryptionHeaderB64 } = await encryptMetadataJSON( - { jsonValue: updatedMagicMetadata.data, keyB64: collection.key }, - ); - const reqBody: UpdateMagicMetadataRequest = { - id: collection.id, - magicMetadata: { - version: updatedMagicMetadata.version, - count: updatedMagicMetadata.count, - data: encryptedDataB64, - header: decryptionHeaderB64, - }, - }; - - await HTTPService.put( - await apiURL("/collections/sharee-magic-metadata"), - reqBody, - null, - { "X-Auth-Token": token }, - ); - const updatedCollection: Collection = { - ...collection, - magicMetadata: { - ...updatedMagicMetadata, - version: updatedMagicMetadata.version + 1, - }, - }; - return updatedCollection; -}; - -export const updatePublicCollectionMagicMetadata = async ( - collection: Collection, - updatedPublicMagicMetadata: CollectionPublicMagicMetadata, -) => { - const token = getToken(); - if (!token) { - return; - } - - const { encryptedDataB64, decryptionHeaderB64 } = await encryptMetadataJSON( - { jsonValue: updatedPublicMagicMetadata.data, keyB64: collection.key }, - ); - const reqBody: UpdateMagicMetadataRequest = { - id: collection.id, - magicMetadata: { - version: updatedPublicMagicMetadata.version, - count: updatedPublicMagicMetadata.count, - data: encryptedDataB64, - header: decryptionHeaderB64, - }, - }; - - await HTTPService.put( - await apiURL("/collections/public-magic-metadata"), - reqBody, - null, - { "X-Auth-Token": token }, - ); - const updatedCollection: Collection = { - ...collection, - pubMagicMetadata: { - ...updatedPublicMagicMetadata, - version: updatedPublicMagicMetadata.version + 1, - }, - }; - return updatedCollection; -}; - export const renameCollection = async ( collection: Collection, newCollectionName: string, -) => { - if (isQuickLinkCollection(collection)) { - // Convert quick link collection to normal collection on rename - await changeCollectionSubType(collection, CollectionSubType.default); - } - const token = getToken(); - const cryptoWorker = await sharedCryptoWorker(); - const { encryptedData: encryptedName, nonce: nameDecryptionNonce } = - await cryptoWorker.encryptUTF8(newCollectionName, collection.key); - const collectionRenameRequest = { - collectionID: collection.id, - encryptedName, - nameDecryptionNonce, - }; - await HTTPService.post( - await apiURL("/collections/rename"), - collectionRenameRequest, - null, - { "X-Auth-Token": token }, - ); -}; - -export const shareCollection = async ( - collection: Collection, - withUserEmail: string, - role: string, -) => { - try { - const cryptoWorker = await sharedCryptoWorker(); - const token = getToken(); - const publicKey: string = await getPublicKey(withUserEmail); - const encryptedKey = await cryptoWorker.boxSeal( - collection.key, - publicKey, - ); - const shareCollectionRequest = { - collectionID: collection.id, - email: withUserEmail, - role: role, - encryptedKey, - }; - await HTTPService.post( - await apiURL("/collections/share"), - shareCollectionRequest, - null, - { "X-Auth-Token": token }, - ); - } catch (e) { - log.error("share collection failed ", e); - throw e; - } -}; - -export const unshareCollection = async ( - collection: Collection, - withUserEmail: string, -) => { - try { - const token = getToken(); - const shareCollectionRequest = { - collectionID: collection.id, - email: withUserEmail, - }; - await HTTPService.post( - await apiURL("/collections/unshare"), - shareCollectionRequest, - null, - { "X-Auth-Token": token }, - ); - } catch (e) { - log.error("unshare collection failed ", e); - } -}; - -export const createShareableURL = async (collection: Collection) => { - try { - const token = getToken(); - if (!token) { - return null; - } - const createPublicAccessTokenRequest: CreatePublicAccessTokenRequest = { - collectionID: collection.id, - }; - const resp = await HTTPService.post( - await apiURL("/collections/share-url"), - createPublicAccessTokenRequest, - null, - { "X-Auth-Token": token }, - ); - return resp.data.result as PublicURL; - } catch (e) { - log.error("createShareableURL failed ", e); - throw e; - } -}; - -export const deleteShareableURL = async (collection: Collection) => { - try { - const token = getToken(); - if (!token) { - return null; - } - await HTTPService.delete( - await apiURL(`/collections/share-url/${collection.id}`), - null, - null, - { "X-Auth-Token": token }, - ); - } catch (e) { - log.error("deleteShareableURL failed ", e); - throw e; - } -}; - -export const updateShareableURL = async ( - request: UpdatePublicURL, -): Promise => { - try { - const token = getToken(); - if (!token) { - return null; - } - const res = await HTTPService.put( - await apiURL("/collections/share-url"), - request, - null, - { "X-Auth-Token": token }, - ); - return res.data.result as PublicURL; - } catch (e) { - log.error("updateShareableURL failed ", e); - throw e; - } -}; +) => renameCollection2(await collection1To2(collection), newCollectionName); +/** + * Return the user's own favorites collection, if any. + */ export const getFavCollection = async () => { const collections = await getLocalCollections(); + const userID = ensureLocalUser().id; for (const collection of collections) { - if (collection.type == "favorites") { + // See: [Note: User and shared favorites] + if (collection.type == "favorites" && collection.owner.id == userID) { return collection; } } @@ -627,9 +379,7 @@ export const sortCollectionSummaries = ( return (b.updationTime ?? 0) - (a.updationTime ?? 0); } }) - // TODO: - // eslint-disable-next-line no-constant-binary-expression - .sort((a, b) => b.order ?? 0 - a.order ?? 0) + .sort((a, b) => (b.order ?? 0) - (a.order ?? 0)) .sort( (a, b) => CollectionSummaryOrder.get(a.type) - @@ -667,9 +417,8 @@ export async function getUncategorizedCollection( return uncategorizedCollection; } -export function createUnCategorizedCollection() { - return createCollection(UNCATEGORIZED_COLLECTION_NAME, "uncategorized"); -} +export const createUnCategorizedCollection = () => + createCollection(uncategorizedCollectionName, "uncategorized"); export async function getDefaultHiddenCollection(): Promise { const collections = await getLocalCollections("hidden"); @@ -680,18 +429,17 @@ export async function getDefaultHiddenCollection(): Promise { return hiddenCollection; } -export function createHiddenCollection() { - return createCollection(HIDDEN_COLLECTION_NAME, "album", { +const createDefaultHiddenCollection = () => + createCollection(defaultHiddenCollectionName, "album", { subType: CollectionSubType.defaultHidden, visibility: ItemVisibility.hidden, }); -} export async function moveToHiddenCollection(files: EnteFile[]) { try { let hiddenCollection = await getDefaultHiddenCollection(); if (!hiddenCollection) { - hiddenCollection = await createHiddenCollection(); + hiddenCollection = await createDefaultHiddenCollection(); } const groupedFiles = groupFilesByCollectionID(files); for (const [collectionID, files] of groupedFiles.entries()) { diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts deleted file mode 100644 index 527023a5cf..0000000000 --- a/web/apps/photos/src/services/export/index.ts +++ /dev/null @@ -1,1516 +0,0 @@ -import { ensureElectron } from "ente-base/electron"; -import { joinPath } from "ente-base/file-name"; -import log from "ente-base/log"; -import { downloadManager } from "ente-gallery/services/download"; -import { writeStream } from "ente-gallery/utils/native-stream"; -import type { Collection } from "ente-media/collection"; -import { mergeMetadata, type EnteFile } from "ente-media/file"; -import { fileLocation, type Metadata } from "ente-media/file-metadata"; -import { FileType } from "ente-media/file-type"; -import { decodeLivePhoto } from "ente-media/live-photo"; -import { - createCollectionNameByID, - getCollectionUserFacingName, -} from "ente-new/photos/services/collection"; -import { getAllLocalCollections } from "ente-new/photos/services/collections"; -import { - exportMetadataDirectoryName, - exportTrashDirectoryName, -} from "ente-new/photos/services/export"; -import { getAllLocalFiles } from "ente-new/photos/services/files"; -import { - safeDirectoryName, - safeFileName, -} from "ente-new/photos/utils/native-fs"; -import { CustomError } from "ente-shared/error"; -import { getData, setData } from "ente-shared/storage/localStorage"; -import { PromiseQueue } from "ente-utils/promise"; -import i18n from "i18next"; -import { migrateExport, type ExportRecord } from "./migration"; - -/** Name of the JSON file in which we keep the state of the export. */ -const exportRecordFileName = "export_status.json"; - -/** - * Name of the top level directory which we create underneath the selected - * directory when the user starts an export to the file system. - */ -const exportDirectoryName = "Ente Photos"; - -export const ExportStage = { - init: 0, - migration: 1, - starting: 2, - exportingFiles: 3, - trashingDeletedFiles: 4, - renamingCollectionFolders: 5, - trashingDeletedCollections: 6, - finished: 7, -} as const; - -export type ExportStage = (typeof ExportStage)[keyof typeof ExportStage]; - -export interface ExportProgress { - success: number; - failed: number; - total: number; -} - -export interface ExportSettings { - folder: string; - continuousExport: boolean; -} - -export type CollectionExportNames = Record; - -export type FileExportNames = Record; - -export const NULL_EXPORT_RECORD: ExportRecord = { - version: 3, - lastAttemptTimestamp: null, - stage: ExportStage.init, - fileExportNames: {}, - collectionExportNames: {}, -}; - -export interface ExportOpts { - /** - * If true, perform an additional on-disk check to determine which files - * need to be exported. - * - * This has performance implications for huge libraries, so we only do this: - * - For the first export after an app start - * - If the user explicitly presses the "Resync" button. - */ - resync?: boolean; -} - -interface ExportUIUpdaters { - setExportStage: (stage: ExportStage) => void; - setExportProgress: (progress: ExportProgress) => void; - setLastExportTime: (exportTime: number) => void; - setPendingExports: (pendingExports: EnteFile[]) => void; -} - -interface RequestCanceller { - exec: () => void; -} - -interface CancellationStatus { - status: boolean; -} - -class ExportService { - private exportSettings: ExportSettings; - private exportInProgress: RequestCanceller = null; - private resync = true; - private reRunNeeded = false; - private exportRecordUpdater = new PromiseQueue(); - private continuousExportEventHandler: () => void; - private uiUpdater: ExportUIUpdaters = { - setExportProgress: () => {}, - setExportStage: () => {}, - setLastExportTime: () => {}, - setPendingExports: () => {}, - }; - private currentExportProgress: ExportProgress = { - total: 0, - success: 0, - failed: 0, - }; - private cachedMetadataDateTimeFormatter: Intl.DateTimeFormat; - - getExportSettings(): ExportSettings { - try { - if (this.exportSettings) { - return this.exportSettings; - } - const exportSettings = getData("export"); - this.exportSettings = exportSettings; - return exportSettings; - } catch (e) { - log.error("getExportSettings failed", e); - throw e; - } - } - - updateExportSettings(newData: Partial) { - try { - const exportSettings = this.getExportSettings(); - const newSettings = { ...exportSettings, ...newData }; - this.exportSettings = newSettings; - setData("export", newSettings); - } catch (e) { - log.error("updateExportSettings failed", e); - throw e; - } - } - - async runMigration( - exportDir: string, - exportRecord: ExportRecord, - updateProgress: (progress: ExportProgress) => void, - ) { - try { - log.info("running migration"); - await migrateExport(exportDir, exportRecord, updateProgress); - log.info("migration completed"); - } catch (e) { - log.error("migration failed", e); - throw e; - } - } - - setUIUpdaters(uiUpdater: ExportUIUpdaters) { - this.uiUpdater = uiUpdater; - this.uiUpdater.setExportProgress(this.currentExportProgress); - } - - private updateExportProgress(exportProgress: ExportProgress) { - this.currentExportProgress = exportProgress; - this.uiUpdater.setExportProgress(exportProgress); - } - - private async updateExportStage(stage: ExportStage) { - const exportFolder = this.getExportSettings()?.folder; - await this.updateExportRecord(exportFolder, { stage }); - this.uiUpdater.setExportStage(stage); - } - - private async updateLastExportTime(exportTime: number) { - const exportFolder = this.getExportSettings()?.folder; - await this.updateExportRecord(exportFolder, { - lastAttemptTimestamp: exportTime, - }); - this.uiUpdater.setLastExportTime(exportTime); - } - - private resyncOnce() { - const resync = this.resync; - this.resync = false; - return resync; - } - - resumeExport() { - this.scheduleExport({ resync: this.resyncOnce() }); - } - - enableContinuousExport() { - if (this.continuousExportEventHandler) { - log.warn("Continuous export already enabled"); - return; - } - this.continuousExportEventHandler = () => { - this.scheduleExport({ resync: this.resyncOnce() }); - }; - this.continuousExportEventHandler(); - } - - disableContinuousExport() { - if (!this.continuousExportEventHandler) { - log.warn("Continuous export already disabled"); - return; - } - this.continuousExportEventHandler = null; - } - - /** - * Called when the local database of files changes. - */ - onLocalFilesUpdated() { - if (this.continuousExportEventHandler) { - this.continuousExportEventHandler(); - } - } - - getPendingExports = async ( - exportRecord: ExportRecord, - ): Promise => { - try { - const files = await getAllLocalFiles(); - - const unExportedFiles = getUnExportedFiles( - files, - exportRecord, - undefined, - ); - return unExportedFiles; - } catch (e) { - log.error("getUpdateFileLists failed", e); - throw e; - } - }; - - async preExport(exportFolder: string) { - await this.verifyExportFolderExists(exportFolder); - const exportRecord = await this.getExportRecord(exportFolder); - await this.updateExportStage(ExportStage.migration); - await this.runMigration( - exportFolder, - exportRecord, - this.updateExportProgress.bind(this), - ); - await this.updateExportStage(ExportStage.starting); - } - - async postExport() { - try { - const exportFolder = this.getExportSettings()?.folder; - if (!(await this.exportFolderExists(exportFolder))) { - this.uiUpdater.setExportStage(ExportStage.init); - return; - } - await this.updateExportStage(ExportStage.finished); - await this.updateLastExportTime(Date.now()); - - const exportRecord = await this.getExportRecord(exportFolder); - - const pendingExports = await this.getPendingExports(exportRecord); - this.uiUpdater.setPendingExports(pendingExports); - } catch (e) { - log.error("postExport failed", e); - } - } - - async stopRunningExport() { - try { - log.info("user requested export cancellation"); - this.exportInProgress.exec(); - this.exportInProgress = null; - this.reRunNeeded = false; - await this.postExport(); - } catch (e) { - log.error("stopRunningExport failed", e); - } - } - - scheduleExport = async (exportOpts: ExportOpts) => { - try { - if (this.exportInProgress) { - log.info("export in progress, scheduling re-run"); - this.reRunNeeded = true; - return; - } else { - log.info("export not in progress, starting export"); - } - - const isCanceled: CancellationStatus = { status: false }; - const canceller: RequestCanceller = { - exec: () => { - isCanceled.status = true; - }, - }; - this.exportInProgress = canceller; - try { - const exportFolder = this.getExportSettings()?.folder; - await this.preExport(exportFolder); - log.info("export started"); - await this.runExport(exportFolder, isCanceled, exportOpts); - log.info("export completed"); - } finally { - if (isCanceled.status) { - log.info("export cancellation done"); - if (!this.exportInProgress) { - await this.postExport(); - } - } else { - await this.postExport(); - log.info("resetting export in progress after completion"); - this.exportInProgress = null; - if (this.reRunNeeded) { - this.reRunNeeded = false; - log.info("re-running export"); - setTimeout(() => this.scheduleExport(exportOpts), 0); - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("scheduleExport failed", e); - } - } - }; - - private async runExport( - exportFolder: string, - isCanceled: CancellationStatus, - { resync }: ExportOpts, - ) { - try { - const files = mergeMetadata(await getAllLocalFiles()); - const collections = await getAllLocalCollections(); - - const exportRecord = await this.getExportRecord(exportFolder); - const collectionIDExportNameMap = - convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - const collectionIDNameMap = createCollectionNameByID(collections); - - const renamedCollections = getRenamedExportedCollections( - collections, - exportRecord, - ); - - const removedFileUIDs = getDeletedExportedFiles( - files, - exportRecord, - ); - - const diskFileRecordIDs = resync - ? await readOnDiskFileExportRecordIDs( - files, - collectionIDExportNameMap, - exportFolder, - exportRecord, - isCanceled, - ) - : undefined; - - const filesToExport = getUnExportedFiles( - files, - exportRecord, - diskFileRecordIDs, - ); - - const deletedExportedCollections = getDeletedExportedCollections( - collections, - exportRecord, - ); - - log.info( - `[export] files: ${files.length}, disk files: ${diskFileRecordIDs?.size ?? ""}, unexported files: ${filesToExport.length}, deleted exported files: ${removedFileUIDs.length}, renamed collections: ${renamedCollections.length}, deleted collections: ${deletedExportedCollections.length}`, - ); - let success = 0; - let failed = 0; - this.uiUpdater.setExportProgress({ - success: success, - failed: failed, - total: filesToExport.length, - }); - const incrementSuccess = () => { - this.updateExportProgress({ - success: ++success, - failed: failed, - total: filesToExport.length, - }); - }; - const incrementFailed = () => { - this.updateExportProgress({ - success: success, - failed: ++failed, - total: filesToExport.length, - }); - }; - if (renamedCollections?.length > 0) { - this.updateExportStage(ExportStage.renamingCollectionFolders); - log.info(`renaming ${renamedCollections.length} collections`); - await this.collectionRenamer( - exportFolder, - collectionIDExportNameMap, - renamedCollections, - isCanceled, - ); - } - - if (removedFileUIDs?.length > 0) { - this.updateExportStage(ExportStage.trashingDeletedFiles); - log.info(`trashing ${removedFileUIDs.length} files`); - await this.fileTrasher( - exportFolder, - collectionIDExportNameMap, - removedFileUIDs, - isCanceled, - ); - } - if (filesToExport?.length > 0) { - this.updateExportStage(ExportStage.exportingFiles); - log.info(`exporting ${filesToExport.length} files`); - await this.fileExporter( - filesToExport, - collectionIDNameMap, - collectionIDExportNameMap, - exportFolder, - incrementSuccess, - incrementFailed, - isCanceled, - ); - } - if (deletedExportedCollections?.length > 0) { - this.updateExportStage(ExportStage.trashingDeletedCollections); - log.info( - `removing ${deletedExportedCollections.length} collections`, - ); - await this.collectionRemover( - deletedExportedCollections, - exportFolder, - isCanceled, - ); - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("runExport failed", e); - } - throw e; - } - } - - async collectionRenamer( - exportFolder: string, - collectionIDExportNameMap: Map, - renamedCollections: Collection[], - isCanceled: CancellationStatus, - ) { - const fs = ensureElectron().fs; - try { - for (const collection of renamedCollections) { - try { - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - await this.verifyExportFolderExists(exportFolder); - const oldCollectionExportName = - collectionIDExportNameMap.get(collection.id); - const oldCollectionExportPath = joinPath( - exportFolder, - oldCollectionExportName, - ); - const newCollectionExportName = await safeDirectoryName( - exportFolder, - getCollectionUserFacingName(collection), - fs.exists, - ); - log.info( - `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName}`, - ); - const newCollectionExportPath = joinPath( - exportFolder, - newCollectionExportName, - ); - await this.addCollectionExportedRecord( - exportFolder, - collection.id, - newCollectionExportName, - ); - collectionIDExportNameMap.set( - collection.id, - newCollectionExportName, - ); - try { - await fs.rename( - oldCollectionExportPath, - newCollectionExportPath, - ); - } catch (e) { - await this.addCollectionExportedRecord( - exportFolder, - collection.id, - oldCollectionExportName, - ); - collectionIDExportNameMap.set( - collection.id, - oldCollectionExportName, - ); - throw e; - } - log.info( - `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName} successful`, - ); - } catch (e) { - log.error("collectionRenamer failed a collection", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("collectionRenamer failed", e); - } - throw e; - } - } - - async collectionRemover( - deletedExportedCollectionIDs: number[], - exportFolder: string, - isCanceled: CancellationStatus, - ) { - const fs = ensureElectron().fs; - const rmdirIfExists = async (dirPath: string) => { - if (await fs.exists(dirPath)) await fs.rmdir(dirPath); - }; - try { - const exportRecord = await this.getExportRecord(exportFolder); - const collectionIDPathMap = - convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - for (const collectionID of deletedExportedCollectionIDs) { - try { - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - await this.verifyExportFolderExists(exportFolder); - log.info( - `removing collection with id ${collectionID} from export folder`, - ); - const collectionExportName = - collectionIDPathMap.get(collectionID); - // verify that the all exported files from the collection has been removed - const collectionExportedFiles = getCollectionExportedFiles( - exportRecord, - collectionID, - ); - if (collectionExportedFiles.length > 0) { - throw new Error( - "collection is not empty, can't remove", - ); - } - const collectionExportPath = joinPath( - exportFolder, - collectionExportName, - ); - await this.removeCollectionExportedRecord( - exportFolder, - collectionID, - ); - try { - // delete the collection metadata folder - await rmdirIfExists( - getMetadataFolderExportPath(collectionExportPath), - ); - // delete the collection folder - await rmdirIfExists(collectionExportPath); - } catch (e) { - await this.addCollectionExportedRecord( - exportFolder, - collectionID, - collectionExportName, - ); - throw e; - } - log.info( - `removing collection with id ${collectionID} from export folder successful`, - ); - } catch (e) { - log.error("collectionRemover failed a collection", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("collectionRemover failed", e); - } - throw e; - } - } - - async fileExporter( - files: EnteFile[], - collectionIDNameMap: Map, - collectionIDFolderNameMap: Map, - exportDir: string, - incrementSuccess: () => void, - incrementFailed: () => void, - isCanceled: CancellationStatus, - ): Promise { - const fs = ensureElectron().fs; - try { - for (const file of files) { - log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( - file.collectionID, - )}`, - ); - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - try { - await this.verifyExportFolderExists(exportDir); - let collectionExportName = collectionIDFolderNameMap.get( - file.collectionID, - ); - if (!collectionExportName) { - collectionExportName = - await this.createNewCollectionExport( - exportDir, - file.collectionID, - collectionIDNameMap, - ); - await this.addCollectionExportedRecord( - exportDir, - file.collectionID, - collectionExportName, - ); - collectionIDFolderNameMap.set( - file.collectionID, - collectionExportName, - ); - } - const collectionExportPath = joinPath( - exportDir, - collectionExportName, - ); - await fs.mkdirIfNeeded(collectionExportPath); - await fs.mkdirIfNeeded( - getMetadataFolderExportPath(collectionExportPath), - ); - await this.downloadAndSave( - exportDir, - collectionExportPath, - file, - ); - incrementSuccess(); - log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( - file.collectionID, - )} successful`, - ); - } catch (e) { - incrementFailed(); - log.error("export failed for a file", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("fileExporter failed", e); - } - throw e; - } - } - - async fileTrasher( - exportDir: string, - collectionIDExportNameMap: Map, - removedFileUIDs: string[], - isCanceled: CancellationStatus, - ): Promise { - try { - const exportRecord = await this.getExportRecord(exportDir); - const fileIDExportNameMap = convertFileIDExportNameObjectToMap( - exportRecord.fileExportNames, - ); - for (const fileUID of removedFileUIDs) { - await this.verifyExportFolderExists(exportDir); - log.info(`trashing file with id ${fileUID}`); - if (isCanceled.status) { - throw Error(CustomError.EXPORT_STOPPED); - } - try { - const fileExportName = fileIDExportNameMap.get(fileUID); - const collectionID = getCollectionIDFromFileUID(fileUID); - const collectionExportName = - collectionIDExportNameMap.get(collectionID); - - if (isLivePhotoExportName(fileExportName)) { - const { image, video } = - parseLivePhotoExportName(fileExportName); - - await moveToFSTrash( - exportDir, - collectionExportName, - image, - ); - - await moveToFSTrash( - exportDir, - collectionExportName, - video, - ); - } else { - await moveToFSTrash( - exportDir, - collectionExportName, - fileExportName, - ); - } - - await this.removeFileExportedRecord(exportDir, fileUID); - - log.info(`Moved file id ${fileUID} to Trash`); - } catch (e) { - log.error("trashing failed for a file", e); - if ( - e.message === - CustomError.UPDATE_EXPORTED_RECORD_FAILED || - e.message === - CustomError.EXPORT_FOLDER_DOES_NOT_EXIST || - e.message === CustomError.EXPORT_STOPPED - ) { - throw e; - } - } - } - } catch (e) { - if ( - e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST && - e.message !== CustomError.EXPORT_STOPPED - ) { - log.error("fileTrasher failed", e); - } - throw e; - } - } - - async addFileExportedRecord( - folder: string, - fileUID: string, - fileExportName: string, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - if (!exportRecord.fileExportNames) { - exportRecord.fileExportNames = {}; - } - exportRecord.fileExportNames = { - ...exportRecord.fileExportNames, - [fileUID]: fileExportName, - }; - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("addFileExportedRecord failed", e); - } - throw e; - } - } - - async addCollectionExportedRecord( - folder: string, - collectionID: number, - collectionExportName: string, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - if (!exportRecord?.collectionExportNames) { - exportRecord.collectionExportNames = {}; - } - exportRecord.collectionExportNames = { - ...exportRecord.collectionExportNames, - [collectionID]: collectionExportName, - }; - - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("addCollectionExportedRecord failed", e); - } - throw e; - } - } - - async removeCollectionExportedRecord(folder: string, collectionID: number) { - try { - const exportRecord = await this.getExportRecord(folder); - - exportRecord.collectionExportNames = Object.fromEntries( - Object.entries(exportRecord.collectionExportNames).filter( - ([key]) => key !== collectionID.toString(), - ), - ); - - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("removeCollectionExportedRecord failed", e); - } - throw e; - } - } - - async removeFileExportedRecord(folder: string, fileUID: string) { - try { - const exportRecord = await this.getExportRecord(folder); - exportRecord.fileExportNames = Object.fromEntries( - Object.entries(exportRecord.fileExportNames).filter( - ([key]) => key !== fileUID, - ), - ); - await this.updateExportRecord(folder, exportRecord); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("removeFileExportedRecord failed", e); - } - throw e; - } - } - - async updateExportRecord(folder: string, newData: Partial) { - return this.exportRecordUpdater.add(() => - this.updateExportRecordHelper(folder, newData), - ); - } - - async updateExportRecordHelper( - folder: string, - newData: Partial, - ) { - try { - const exportRecord = await this.getExportRecord(folder); - const newRecord: ExportRecord = { ...exportRecord, ...newData }; - await ensureElectron().fs.writeFileViaBackup( - joinPath(folder, exportRecordFileName), - JSON.stringify(newRecord, null, 2), - ); - return newRecord; - } catch (e) { - if (e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - throw e; - } - log.error("error updating Export Record", e); - throw Error(CustomError.UPDATE_EXPORTED_RECORD_FAILED); - } - } - - async getExportRecord(folder: string): Promise { - const electron = ensureElectron(); - const fs = electron.fs; - try { - await this.verifyExportFolderExists(folder); - const exportRecordJSONPath = joinPath(folder, exportRecordFileName); - if (!(await fs.exists(exportRecordJSONPath))) { - return await this.createEmptyExportRecord(exportRecordJSONPath); - } - const recordFile = await fs.readTextFile(exportRecordJSONPath); - return JSON.parse(recordFile); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("export Record JSON parsing failed", e); - } - throw e; - } - } - - async createNewCollectionExport( - exportFolder: string, - collectionID: number, - collectionIDNameMap: Map, - ) { - const fs = ensureElectron().fs; - await this.verifyExportFolderExists(exportFolder); - const collectionName = collectionIDNameMap.get(collectionID); - const collectionExportName = await safeDirectoryName( - exportFolder, - collectionName, - fs.exists, - ); - const collectionExportPath = joinPath( - exportFolder, - collectionExportName, - ); - await fs.mkdirIfNeeded(collectionExportPath); - await fs.mkdirIfNeeded( - getMetadataFolderExportPath(collectionExportPath), - ); - - return collectionExportName; - } - - async downloadAndSave( - exportDir: string, - collectionExportPath: string, - file: EnteFile, - ): Promise { - const electron = ensureElectron(); - try { - const fileUID = getExportRecordFileUID(file); - const originalFileStream = await downloadManager.fileStream(file); - if (file.metadata.fileType === FileType.livePhoto) { - await this.exportLivePhoto( - exportDir, - fileUID, - collectionExportPath, - originalFileStream, - file, - ); - } else { - const fileExportName = await safeFileName( - collectionExportPath, - file.metadata.title, - electron.fs.exists, - ); - await this.saveMetadataFile( - collectionExportPath, - fileExportName, - file, - ); - await writeStream( - electron, - joinPath(collectionExportPath, fileExportName), - originalFileStream, - ); - await this.addFileExportedRecord( - exportDir, - fileUID, - fileExportName, - ); - } - } catch (e) { - log.error("download and save failed", e); - throw e; - } - } - - private async exportLivePhoto( - exportDir: string, - fileUID: string, - collectionExportPath: string, - fileStream: ReadableStream, - file: EnteFile, - ) { - const fs = ensureElectron().fs; - const fileBlob = await new Response(fileStream).blob(); - const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); - const imageExportName = await safeFileName( - collectionExportPath, - livePhoto.imageFileName, - fs.exists, - ); - const videoExportName = await safeFileName( - collectionExportPath, - livePhoto.videoFileName, - fs.exists, - ); - - const livePhotoExportName = getLivePhotoExportName( - imageExportName, - videoExportName, - ); - - await this.saveMetadataFile( - collectionExportPath, - imageExportName, - file, - ); - await writeStream( - electron, - joinPath(collectionExportPath, imageExportName), - new Response(livePhoto.imageData).body, - ); - - await this.saveMetadataFile( - collectionExportPath, - videoExportName, - file, - ); - try { - await writeStream( - electron, - joinPath(collectionExportPath, videoExportName), - new Response(livePhoto.videoData).body, - ); - } catch (e) { - await fs.rm(joinPath(collectionExportPath, imageExportName)); - throw e; - } - - await this.addFileExportedRecord( - exportDir, - fileUID, - livePhotoExportName, - ); - } - - private async saveMetadataFile( - collectionExportPath: string, - fileExportName: string, - file: EnteFile, - ) { - const formatter = this.metadataDateTimeFormatter(); - await ensureElectron().fs.writeFile( - getFileMetadataExportPath(collectionExportPath, fileExportName), - getGoogleLikeMetadataFile(fileExportName, file, formatter), - ); - } - - /** - * Lazily created, cached instance of the date time formatter that should be - * used for formatting the dates added to the metadata file. - */ - private metadataDateTimeFormatter() { - if (this.cachedMetadataDateTimeFormatter) - return this.cachedMetadataDateTimeFormatter; - - // AFAIK, Google's format is not documented. It also seems to vary with - // locale. This is a best attempt at constructing a formatter that - // mirrors the format used by the timestamps in the takeout JSON. - const formatter = new Intl.DateTimeFormat(i18n.language, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - timeZoneName: "short", - timeZone: "UTC", - }); - this.cachedMetadataDateTimeFormatter = formatter; - return formatter; - } - - isExportInProgress = () => { - return this.exportInProgress; - }; - - exportFolderExists = async (exportFolder: string) => { - return exportFolder && (await ensureElectron().fs.exists(exportFolder)); - }; - - private verifyExportFolderExists = async (exportFolder: string) => { - try { - if (!(await this.exportFolderExists(exportFolder))) { - throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); - } - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("verifyExportFolderExists failed", e); - } - throw e; - } - }; - - private createEmptyExportRecord = async (exportRecordJSONPath: string) => { - const exportRecord: ExportRecord = NULL_EXPORT_RECORD; - await ensureElectron().fs.writeFile( - exportRecordJSONPath, - JSON.stringify(exportRecord, null, 2), - ); - return exportRecord; - }; -} - -const exportService = new ExportService(); - -export default exportService; - -/** - * If there are any in-progress exports, or if continuous exports are enabled, - * resume them. - */ -export const resumeExportsIfNeeded = async () => { - const exportSettings = exportService.getExportSettings(); - if (!(await exportService.exportFolderExists(exportSettings?.folder))) { - return; - } - const exportRecord = await exportService.getExportRecord( - exportSettings.folder, - ); - if (exportSettings.continuousExport) { - exportService.enableContinuousExport(); - } - if (isExportInProgress(exportRecord.stage)) { - log.debug(() => "Resuming in-progress export"); - exportService.resumeExport(); - } -}; - -/** - * Prompt the user to select a directory and create an export directory in it. - * - * If the user cancels the selection, return undefined. - */ -export const selectAndPrepareExportDirectory = async (): Promise< - string | undefined -> => { - const electron = ensureElectron(); - - const rootDir = await electron.selectDirectory(); - if (!rootDir) return undefined; - - const exportDir = joinPath(rootDir, exportDirectoryName); - await electron.fs.mkdirIfNeeded(exportDir); - return exportDir; -}; - -export const getExportRecordFileUID = (file: EnteFile) => - `${file.id}_${file.collectionID}_${file.updationTime}`; - -export const getCollectionIDFromFileUID = (fileUID: string) => - Number(fileUID.split("_")[1]); - -const convertCollectionIDExportNameObjectToMap = ( - collectionExportNames: CollectionExportNames, -): Map => { - return new Map( - Object.entries(collectionExportNames ?? {}).map((e) => { - return [Number(e[0]), String(e[1])]; - }), - ); -}; - -const convertFileIDExportNameObjectToMap = ( - fileExportNames: FileExportNames, -): Map => { - return new Map( - Object.entries(fileExportNames ?? {}).map((e) => { - return [String(e[0]), String(e[1])]; - }), - ); -}; - -const getRenamedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - const renamedCollections = collections.filter((collection) => { - if (collectionIDExportNameMap.has(collection.id)) { - const currentExportName = collectionIDExportNameMap.get( - collection.id, - ); - - const collectionExportName = - getCollectionUserFacingName(collection); - - if (currentExportName === collectionExportName) { - return false; - } - const hasNumberedSuffix = /\(\d+\)$/.exec(currentExportName); - const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix - ? currentExportName.replace(/\(\d+\)$/, "") - : currentExportName; - - return ( - collectionExportName !== currentExportNameWithoutNumberedSuffix - ); - } - return false; - }); - return renamedCollections; -}; - -const getDeletedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const presentCollections = new Set( - collections.map((collection) => collection.id), - ); - const deletedExportedCollections = Object.keys( - exportRecord?.collectionExportNames, - ) - .map(Number) - .filter((collectionID) => { - if (!presentCollections.has(collectionID)) { - return true; - } - return false; - }); - return deletedExportedCollections; -}; - -/** - * Return export record IDs of {@link files} for which there is also exists a - * file on disk. - */ -const readOnDiskFileExportRecordIDs = async ( - files: EnteFile[], - collectionIDFolderNameMap: Map, - exportDir: string, - exportRecord: ExportRecord, - isCanceled: CancellationStatus, -): Promise> => { - const fs = ensureElectron().fs; - - const result = new Set(); - if (!(await fs.exists(exportDir))) return result; - - // Both the paths involved are guaranteed to use POSIX separators and thus - // can directly be compared. - // - // - `exportDir` traces its origin to `electron.selectDirectory()`, which - // returns POSIX paths. Down below we use it as the base directory when - // constructing paths for the items to export. - // - // - `findFiles` is also guaranteed to return POSIX paths. - // - const ls = new Set(await ensureElectron().fs.findFiles(exportDir)); - - const fileExportNames = exportRecord.fileExportNames ?? {}; - - for (const file of files) { - if (isCanceled.status) throw Error(CustomError.EXPORT_STOPPED); - - const collectionExportName = collectionIDFolderNameMap.get( - file.collectionID, - ); - if (!collectionExportName) continue; - - const collectionExportPath = joinPath(exportDir, collectionExportName); - const recordID = getExportRecordFileUID(file); - const exportName = fileExportNames[recordID]; - if (!exportName) continue; - - if (ls.has(joinPath(collectionExportPath, exportName))) { - result.add(recordID); - } else { - // It might be a live photo - these store a JSON string instead of - // the file's name as the exportName. - try { - const { image, video } = parseLivePhotoExportName(exportName); - if ( - ls.has(joinPath(collectionExportPath, image)) && - ls.has(joinPath(collectionExportPath, video)) - ) { - result.add(recordID); - } - } catch { - /* Not an error, the file just might not exist on disk yet */ - } - } - } - - return result; -}; - -/** - * Return the list of files from amongst {@link allFiles} that still need to be - * exported. - * - * @param allFiles The list of files to export. - * - * @param exportRecord The export record containing bookkeeping for the export. - * - * @paramd diskFileRecordIDs (Optional) The export record IDs of files from - * amongst {@link allFiles} that already exist on disk. If provided (e.g. when - * doing a resync), we perform an extra check for on-disk existence instead of - * relying solely on the export record. - */ -const getUnExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, - diskFileRecordIDs: Set | undefined, -) => { - if (!exportRecord?.fileExportNames) { - return allFiles; - } - const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames)); - return allFiles.filter((file) => { - const recordID = getExportRecordFileUID(file); - if (!exportedFiles.has(recordID)) return true; - if (diskFileRecordIDs && !diskFileRecordIDs.has(recordID)) return true; - return false; - }); -}; - -const getDeletedExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const presentFileUIDs = new Set( - allFiles?.map((file) => getExportRecordFileUID(file)), - ); - const deletedExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - if (!presentFileUIDs.has(fileUID)) { - return true; - } - return false; - }); - return deletedExportedFiles; -}; - -const getCollectionExportedFiles = ( - exportRecord: ExportRecord, - collectionID: number, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const collectionExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - const fileCollectionID = Number(fileUID.split("_")[1]); - if (fileCollectionID === collectionID) { - return true; - } else { - return false; - } - }); - return collectionExportedFiles; -}; - -const getGoogleLikeMetadataFile = ( - fileExportName: string, - file: EnteFile, - dateTimeFormatter: Intl.DateTimeFormat, -) => { - const metadata: Metadata = file.metadata; - const publicMagicMetadata = file.pubMagicMetadata?.data; - const creationTime = Math.floor( - (publicMagicMetadata?.editedTime ?? metadata.creationTime) / 1e6, - ); - const modificationTime = Math.floor( - (metadata.modificationTime ?? metadata.creationTime) / 1e6, - ); - const result: Record = { - title: fileExportName, - creationTime: { - timestamp: `${creationTime}`, - formatted: dateTimeFormatter.format(creationTime * 1000), - }, - modificationTime: { - timestamp: `${modificationTime}`, - formatted: dateTimeFormatter.format(modificationTime * 1000), - }, - }; - const caption = file?.pubMagicMetadata?.data?.caption; - if (caption) result.caption = caption; - const geoData = fileLocation(file); - if (geoData) result.geoData = geoData; - return JSON.stringify(result, null, 2); -}; - -export const getMetadataFolderExportPath = (collectionExportPath: string) => - joinPath(collectionExportPath, exportMetadataDirectoryName); - -// if filepath is /home/user/Ente/Export/Collection1/1.jpg -// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json -const getFileMetadataExportPath = ( - collectionExportPath: string, - fileExportName: string, -) => - joinPath( - collectionExportPath, - joinPath(exportMetadataDirectoryName, `${fileExportName}.json`), - ); - -export const getLivePhotoExportName = ( - imageExportName: string, - videoExportName: string, -) => JSON.stringify({ image: imageExportName, video: videoExportName }); - -export const isLivePhotoExportName = (exportName: string) => { - try { - JSON.parse(exportName); - return true; - } catch { - return false; - } -}; - -const parseLivePhotoExportName = ( - livePhotoExportName: string, -): { image: string; video: string } => { - const { image, video } = JSON.parse(livePhotoExportName); - return { image, video }; -}; - -const isExportInProgress = (exportStage: ExportStage) => - exportStage > ExportStage.init && exportStage < ExportStage.finished; - -/** - * Move {@link fileName} in {@link collectionName} to the special per-collection - * file system "Trash" folder we created under the export directory. - * - * Also move its associated metadata JSON to Trash. - * - * @param exportDir The root directory on the user's file system where we are - * exporting to. - * */ -const moveToFSTrash = async ( - exportDir: string, - collectionName: string, - fileName: string, -) => { - const fs = ensureElectron().fs; - - const filePath = joinPath(exportDir, joinPath(collectionName, fileName)); - const trashDir = joinPath( - exportDir, - joinPath(exportTrashDirectoryName, collectionName), - ); - const metadataFileName = `${fileName}.json`; - const metadataFilePath = joinPath( - exportDir, - joinPath( - collectionName, - joinPath(exportMetadataDirectoryName, metadataFileName), - ), - ); - const metadataTrashDir = joinPath( - exportDir, - joinPath( - exportTrashDirectoryName, - joinPath(collectionName, exportMetadataDirectoryName), - ), - ); - - log.info(`Moving file ${filePath} and its metadata to trash folder`); - - if (await fs.exists(filePath)) { - await fs.mkdirIfNeeded(trashDir); - const trashFileName = await safeFileName(trashDir, fileName, fs.exists); - const trashFilePath = joinPath(trashDir, trashFileName); - await fs.rename(filePath, trashFilePath); - } - - if (await fs.exists(metadataFilePath)) { - await fs.mkdirIfNeeded(metadataTrashDir); - const metadataTrashFileName = await safeFileName( - metadataTrashDir, - metadataFileName, - fs.exists, - ); - const metadataTrashFilePath = joinPath( - metadataTrashDir, - metadataTrashFileName, - ); - await fs.rename(metadataFilePath, metadataTrashFilePath); - } -}; diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index 6883eff11f..c70f3a8b54 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -7,12 +7,12 @@ import { logoutFileViewerDataSource } from "ente-gallery/components/viewer/data- import { downloadManager } from "ente-gallery/services/download"; import { resetUploadState } from "ente-gallery/services/upload"; import { resetVideoState } from "ente-gallery/services/video"; +import exportService from "ente-new/photos/services/export"; import { logoutML, terminateMLWorker } from "ente-new/photos/services/ml"; import { logoutSearch } from "ente-new/photos/services/search"; import { logoutSettings } from "ente-new/photos/services/settings"; import { logoutUserDetails } from "ente-new/photos/services/user-details"; -import exportService from "./export"; -import uploadManager from "./upload/uploadManager"; +import { uploadManager } from "./upload-manager"; /** * Logout sequence for the photos app. diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 73c6a1ce31..b263bab226 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -325,22 +325,28 @@ export const getPublicCollection = async ( const collectionName = (fetchedCollection.name = fetchedCollection.name || - (await cryptoWorker.decryptToUTF8( - fetchedCollection.encryptedName, - fetchedCollection.nameDecryptionNonce, - collectionKey, - ))); + new TextDecoder().decode( + await cryptoWorker.decryptBoxBytes( + { + encryptedData: fetchedCollection.encryptedName, + nonce: fetchedCollection.nameDecryptionNonce, + }, + collectionKey, + ), + )); let collectionPublicMagicMetadata: CollectionPublicMagicMetadata; if (fetchedCollection.pubMagicMetadata?.data) { collectionPublicMagicMetadata = { ...fetchedCollection.pubMagicMetadata, - data: await cryptoWorker.decryptMetadataJSON({ - encryptedDataB64: fetchedCollection.pubMagicMetadata.data, - decryptionHeaderB64: - fetchedCollection.pubMagicMetadata.header, - keyB64: collectionKey, - }), + data: await cryptoWorker.decryptMetadataJSON( + { + encryptedData: fetchedCollection.pubMagicMetadata.data, + decryptionHeader: + fetchedCollection.pubMagicMetadata.header, + }, + collectionKey, + ), }; } diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload-manager.ts similarity index 87% rename from web/apps/photos/src/services/upload/uploadManager.ts rename to web/apps/photos/src/services/upload-manager.ts index c2ee02feed..6cce6aaf43 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -1,4 +1,3 @@ -import { Canceler } from "axios"; import { isDesktop } from "ente-base/app"; import { createComlinkCryptoWorker } from "ente-base/crypto"; import { type CryptoWorker } from "ente-base/crypto/worker"; @@ -7,13 +6,25 @@ import type { PublicAlbumsCredentials } from "ente-base/http"; import log from "ente-base/log"; import { ComlinkWorker } from "ente-base/worker/comlink-worker"; import { - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, markUploadedAndObtainProcessableItem, shouldDisableCFUploadProxy, type ClusteredUploadItem, type UploadPhase, type UploadResult, + type UploadableUploadItem, } from "ente-gallery/services/upload"; +import { + metadataJSONMapKeyForJSON, + tryParseTakeoutMetadataJSON, + type ParsedMetadataJSON, +} from "ente-gallery/services/upload/metadata-json"; +import UploadService, { + areLivePhotoAssets, + upload, + uploadItemFileName, + type PotentialLivePhotoAsset, + type UploadAsset, +} from "ente-gallery/services/upload/upload-service"; import { processVideoNewUpload } from "ente-gallery/services/video"; import type { Collection } from "ente-media/collection"; import { @@ -34,18 +45,6 @@ import { } from "services/publicCollectionService"; import watcher from "services/watch"; import { getUserOwnedFiles } from "utils/file"; -import { - metadataJSONMapKeyForJSON, - tryParseTakeoutMetadataJSON, - type ParsedMetadataJSON, -} from "./takeout"; -import UploadService, { - areLivePhotoAssets, - uploadItemFileName, - uploader, - type PotentialLivePhotoAsset, - type UploadAsset, -} from "./upload-service"; export type FileID = number; @@ -63,16 +62,23 @@ export interface InProgressUpload { progress: PercentageUploaded; } +/** + * A variant of {@link UploadResult} used when segregating finished uploads in + * the UI. "addedSymlink" is treated as "uploaded", everything else remains as + * it were. + */ +export type FinishedUploadResult = Exclude; + export interface FinishedUpload { localFileID: FileID; - result: UploadResult; + result: FinishedUploadResult; } export type InProgressUploads = Map; -export type FinishedUploads = Map; +export type FinishedUploads = Map; -export type SegregatedFinishedUploads = Map; +export type SegregatedFinishedUploads = Map; export interface ProgressUpdater { setPercentComplete: React.Dispatch>; @@ -157,7 +163,7 @@ class UIService { this.setTotalFileCount(count); this.filesUploadedCount = 0; this.inProgressUploads = new Map(); - this.finishedUploads = new Map(); + this.finishedUploads = new Map(); this.updateProgressBarUI(); } @@ -201,7 +207,7 @@ class UIService { this.updateProgressBarUI(); } - moveFileToResultList(key: number, uploadResult: UploadResult) { + moveFileToResultList(key: number, uploadResult: FinishedUploadResult) { this.finishedUploads.set(key, uploadResult); this.inProgressUploads.delete(key); this.updateProgressBarUI(); @@ -243,48 +249,16 @@ class UIService { setFinishedUploads(groupByResult(this.finishedUploads)); } - trackUploadProgress( - fileLocalID: number, - percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(), - index = 0, - ) { - const cancel: { exec: Canceler } = { exec: () => {} }; - const cancelTimedOutRequest = () => cancel.exec("Request timed out"); - - const cancelCancelledUploadRequest = () => - cancel.exec(CustomError.UPLOAD_CANCELLED); - - let timeout = null; - const resetTimeout = () => { - if (timeout) { - clearTimeout(timeout); - } - timeout = setTimeout(cancelTimedOutRequest, 30 * 1000 /* 30 sec */); - }; - return { - cancel, - onUploadProgress: (event) => { - this.inProgressUploads.set( - fileLocalID, - Math.min( - Math.round( - percentPerPart * index + - (percentPerPart * event.loaded) / event.total, - ), - 98, - ), - ); - this.updateProgressBarUI(); - if (event.loaded === event.total) { - clearTimeout(timeout); - } else { - resetTimeout(); - } - if (uploadCancelService.isUploadCancelationRequested()) { - cancelCancelledUploadRequest(); - } - }, - }; + /** + * Update the upload progress shown in the UI to {@link percentage} for the + * file with the given {@link fileLocalID}. + * + * @param percentage The upload completion percentage. It should be a value + * between 0 and 100 (inclusive). + */ + updateUploadProgress(fileLocalID: number, percentage: number) { + this.inProgressUploads.set(fileLocalID, Math.round(percentage)); + this.updateProgressBarUI(); } } @@ -351,10 +325,17 @@ class UploadManager { this.uploaderName = null; } - public prepareForNewUpload() { + public prepareForNewUpload( + parsedMetadataJSONMap?: Map, + ) { this.resetState(); this.uiService.reset(); uploadCancelService.reset(); + + if (parsedMetadataJSONMap) { + this.parsedMetadataJSONMap = parsedMetadataJSONMap; + } + this.uiService.setUploadPhase("preparing"); } @@ -473,6 +454,7 @@ class UploadManager { const item = { uploadItem: file, + pathPrefix: undefined, localID: 1, collectionID: collection.id, externalParsedMetadata: { creationDate, creationTime }, @@ -507,16 +489,19 @@ class UploadManager { ) { this.uiService.reset(items.length); - for (const { uploadItem, fileName, collectionID } of items) { + for (const item of items) { this.abortIfCancelled(); + const { uploadItem, pathPrefix, fileName, collectionID } = item; log.info(`Parsing metadata JSON ${fileName}`); const metadataJSON = await tryParseTakeoutMetadataJSON(uploadItem!); if (metadataJSON) { - this.parsedMetadataJSONMap.set( - metadataJSONMapKeyForJSON(collectionID, fileName), - metadataJSON, + const key = metadataJSONMapKeyForJSON( + pathPrefix, + collectionID, + fileName, ); + this.parsedMetadataJSONMap.set(key, metadataJSON); this.uiService.increaseFileUploaded(); } } @@ -543,6 +528,12 @@ class UploadManager { private async uploadNextItemInQueue(worker: CryptoWorker) { const uiService = this.uiService; + const uploadContext = { + isCFUploadProxyDisabled: shouldDisableCFUploadProxy(), + abortIfCancelled: this.abortIfCancelled.bind(this), + updateUploadProgress: + uiService.updateUploadProgress.bind(uiService), + }; while (this.itemsToBeUploaded.length > 0) { this.abortIfCancelled(); @@ -556,26 +547,13 @@ class UploadManager { uiService.setFileProgress(localID, 0); await wait(0); - const { uploadResult, uploadedFile } = await uploader( + const { uploadResult, uploadedFile } = await upload( uploadableItem, this.uploaderName, this.existingFiles, this.parsedMetadataJSONMap, worker, - shouldDisableCFUploadProxy(), - () => { - this.abortIfCancelled(); - }, - ( - fileLocalID: number, - percentPerPart?: number, - index?: number, - ) => - uiService.trackUploadProgress( - fileLocalID, - percentPerPart, - index, - ), + uploadContext, ); const finalUploadResult = await this.postUploadTask( @@ -584,8 +562,8 @@ class UploadManager { uploadedFile, ); - this.uiService.moveFileToResultList(localID, finalUploadResult); - this.uiService.increaseFileUploaded(); + uiService.moveFileToResultList(localID, finalUploadResult); + uiService.increaseFileUploaded(); UploadService.reducePendingUploadCount(); } } @@ -594,8 +572,10 @@ class UploadManager { uploadableItem: UploadableUploadItem, uploadResult: UploadResult, uploadedFile: EncryptedEnteFile | EnteFile | undefined, - ) { + ): Promise { log.info(`Upload ${uploadableItem.fileName} | ${uploadResult}`); + const finishedUploadResult = + uploadResult == "addedSymlink" ? "uploaded" : uploadResult; try { const processableUploadItem = await markUploadedAndObtainProcessableItem(uploadableItem); @@ -607,11 +587,8 @@ class UploadManager { this.failedItems.push(uploadableItem); break; case "alreadyUploaded": - decryptedFile = uploadedFile as EnteFile; - break; case "addedSymlink": decryptedFile = uploadedFile as EnteFile; - uploadResult = "uploaded"; break; case "uploaded": case "uploadedWithStaticThumbnail": @@ -652,7 +629,7 @@ class UploadManager { uploadableItem, uploadedFile as EncryptedEnteFile, ); - return uploadResult; + return finishedUploadResult; } catch (e) { log.error("Post file upload action failed", e); return "failed"; @@ -681,10 +658,15 @@ class UploadManager { uploadCancelService.requestUploadCancelation(); } - public getFailedItemsWithCollections() { + /** + * Return the list of failed items from the last upload, along with other + * state needed to attempt to reupload them. + */ + public failedItemState() { return { - items: this.failedItems, + items: [...this.failedItems], collections: [...this.collections.values()], + parsedMetadataJSONMap: this.parsedMetadataJSONMap, }; } @@ -700,12 +682,20 @@ class UploadManager { this.onUploadFile(decryptedFile); } - public shouldAllowNewUpload = () => { - return !this.uploadInProgress || watcher.isUploadRunning(); + /** + * `true` if an upload is currently in-progress (either a bunch of files + * directly uploaded by the user, or files being uploaded by the folder + * watch functionality). + */ + public isUploadInProgress = () => { + return this.uploadInProgress || watcher.isUploadRunning(); }; } -export default new UploadManager(); +/** + * Singleton instance of {@link UploadManager}. + */ +export const uploadManager = new UploadManager(); /** * The data operated on by the intermediate stages of the upload. @@ -731,7 +721,7 @@ export default new UploadManager(); * * - On to the {@link ClusteredUploadItem} we attach the corresponding * {@link collection}, giving us {@link UploadableUploadItem}. This is what - * gets queued and then passed to the {@link uploader}. + * gets queued and then passed to the {@link upload}. */ type UploadItemWithCollectionIDAndName = UploadAsset & { /** A unique ID for the duration of the upload */ @@ -756,20 +746,11 @@ const makeUploadItemWithCollectionIDAndName = ( : uploadItemFileName(f.uploadItem))!, isLivePhoto: f.isLivePhoto, uploadItem: f.uploadItem, + pathPrefix: f.pathPrefix, livePhotoAssets: f.livePhotoAssets, externalParsedMetadata: f.externalParsedMetadata, }); -/** - * The file that we hand off to the uploader. Essentially - * {@link ClusteredUploadItem} with the {@link collection} attached to it. - * - * See: [Note: Intermediate file types during upload]. - */ -export type UploadableUploadItem = ClusteredUploadItem & { - collection: Collection; -}; - const splitMetadataAndMediaItems = ( items: UploadItemWithCollectionIDAndName[], ): [ @@ -812,12 +793,14 @@ const clusterLivePhotos = async ( fileType: fFileType, collectionID: f.collectionID, uploadItem: f.uploadItem, + pathPrefix: f.pathPrefix, }; const ga: PotentialLivePhotoAsset = { fileName: g.fileName, fileType: gFileType, collectionID: g.collectionID, uploadItem: g.uploadItem, + pathPrefix: g.pathPrefix, }; if (await areLivePhotoAssets(fa, ga, parsedMetadataJSONMap)) { const [image, video] = @@ -827,6 +810,7 @@ const clusterLivePhotos = async ( collectionID: f.collectionID, fileName: image.fileName, isLivePhoto: true, + pathPrefix: image.pathPrefix, livePhotoAssets: { image: image.uploadItem, video: video.uploadItem, @@ -834,12 +818,15 @@ const clusterLivePhotos = async ( }); index += 2; } else { - result.push({ ...f, isLivePhoto: false }); + // They may already be a live photo (we might be retrying a + // previously failed upload). + result.push({ ...f, isLivePhoto: f.isLivePhoto ?? false }); index += 1; } } - if (index === items.length - 1) { - result.push({ ...items[index], isLivePhoto: false }); + if (index == items.length - 1) { + const f = items[index]!; + result.push({ ...f, isLivePhoto: f.isLivePhoto ?? false }); } return result; }; diff --git a/web/apps/photos/src/services/upload/remote.ts b/web/apps/photos/src/services/upload/remote.ts deleted file mode 100644 index 632532e5c0..0000000000 --- a/web/apps/photos/src/services/upload/remote.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { - authenticatedPublicAlbumsRequestHeaders, - authenticatedRequestHeaders, - ensureOk, - retryAsyncOperation, - type PublicAlbumsCredentials, -} from "ente-base/http"; -import log from "ente-base/log"; -import { apiURL, uploaderOrigin } from "ente-base/origins"; -import { EnteFile } from "ente-media/file"; -import { CustomError, handleUploadError } from "ente-shared/error"; -import HTTPService from "ente-shared/network/HTTPService"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; -import { z } from "zod"; -import { MultipartUploadURLs, UploadFile } from "./upload-service"; - -/** - * A pre-signed URL alongwith the associated object key. - */ -const ObjectUploadURL = z.object({ - /** A pre-signed URL that can be used to upload data to S3. */ - objectKey: z.string(), - /** The objectKey with which remote will refer to this object. */ - url: z.string(), -}); - -export type ObjectUploadURL = z.infer; - -const ObjectUploadURLResponse = z.object({ urls: ObjectUploadURL.array() }); - -export class PhotosUploadHttpClient { - async uploadFile(uploadFile: UploadFile): Promise { - try { - const token = getToken(); - if (!token) { - return; - } - const url = await apiURL("/files"); - const response = await retryAsyncOperation( - () => - HTTPService.post(url, uploadFile, null, { - "X-Auth-Token": token, - }), - handleUploadError, - ); - return response.data; - } catch (e) { - log.error("upload Files Failed", e); - throw e; - } - } - - /** - * Fetch a fresh list of URLs from remote that can be used to upload files - * and thumbnails to. - * - * @param countHint An approximate number of files that we're expecting to - * upload. - * - * @returns A list of pre-signed object URLs that can be used to upload data - * to the S3 bucket. - */ - async fetchUploadURLs(countHint: number) { - const count = Math.min(50, countHint * 2).toString(); - const params = new URLSearchParams({ count }); - const url = await apiURL("/files/upload-urls"); - const res = await fetch(`${url}?${params.toString()}`, { - headers: await authenticatedRequestHeaders(), - }); - ensureOk(res); - return ( - // TODO: The as cast will not be needed when tsc strict mode is - // enabled for this code. - ObjectUploadURLResponse.parse(await res.json()) - .urls as ObjectUploadURL[] - ); - } - - async fetchMultipartUploadURLs( - count: number, - ): Promise { - try { - const token = getToken(); - if (!token) { - return; - } - const response = await HTTPService.get( - await apiURL("/files/multipart-upload-urls"), - { count }, - { "X-Auth-Token": token }, - ); - - return response.data.urls; - } catch (e) { - log.error("fetch multipart-upload-url failed", e); - throw e; - } - } - - async putFile( - fileUploadURL: ObjectUploadURL, - file: Uint8Array, - progressTracker, - ): Promise { - try { - await retryAsyncOperation( - () => - HTTPService.put( - fileUploadURL.url, - file, - null, - null, - progressTracker, - ), - handleUploadError, - ); - return fileUploadURL.objectKey; - } catch (e) { - if (e.message !== CustomError.UPLOAD_CANCELLED) { - log.error("putFile to dataStore failed ", e); - } - throw e; - } - } - - async putFileV2( - fileUploadURL: ObjectUploadURL, - file: Uint8Array, - progressTracker, - ): Promise { - try { - const origin = await uploaderOrigin(); - await retryAsyncOperation(() => - HTTPService.put( - `${origin}/file-upload`, - file, - null, - { "UPLOAD-URL": fileUploadURL.url }, - progressTracker, - ), - ); - return fileUploadURL.objectKey; - } catch (e) { - if (e.message !== CustomError.UPLOAD_CANCELLED) { - log.error("putFile to dataStore failed ", e); - } - throw e; - } - } - - async putFilePart( - partUploadURL: string, - filePart: Uint8Array, - progressTracker, - ) { - try { - const response = await retryAsyncOperation(async () => { - const resp = await HTTPService.put( - partUploadURL, - filePart, - null, - null, - progressTracker, - ); - if (!resp?.headers?.etag) { - const err = Error(CustomError.ETAG_MISSING); - log.error("putFile in parts failed", err); - throw err; - } - return resp; - }, handleUploadError); - return response.headers.etag as string; - } catch (e) { - if (e.message !== CustomError.UPLOAD_CANCELLED) { - log.error("put filePart failed", e); - } - throw e; - } - } - - async putFilePartV2( - partUploadURL: string, - filePart: Uint8Array, - progressTracker, - ) { - try { - const origin = await uploaderOrigin(); - const response = await retryAsyncOperation(async () => { - const resp = await HTTPService.put( - `${origin}/multipart-upload`, - filePart, - null, - { "UPLOAD-URL": partUploadURL }, - progressTracker, - ); - if (!resp?.data?.etag) { - const err = Error(CustomError.ETAG_MISSING); - log.error("putFile in parts failed", err); - throw err; - } - return resp; - }); - return response.data.etag as string; - } catch (e) { - if (e.message !== CustomError.UPLOAD_CANCELLED) { - log.error("put filePart failed", e); - } - throw e; - } - } - - async completeMultipartUpload(completeURL: string, reqBody: any) { - try { - await retryAsyncOperation(() => - HTTPService.post(completeURL, reqBody, null, { - "content-type": "text/xml", - }), - ); - } catch (e) { - log.error("put file in parts failed", e); - throw e; - } - } - - async completeMultipartUploadV2(completeURL: string, reqBody: any) { - try { - const origin = await uploaderOrigin(); - await retryAsyncOperation(() => - HTTPService.post( - `${origin}/multipart-complete`, - reqBody, - null, - { "content-type": "text/xml", "UPLOAD-URL": completeURL }, - ), - ); - } catch (e) { - log.error("put file in parts failed", e); - throw e; - } - } -} - -export class PublicUploadHttpClient { - async uploadFile( - uploadFile: UploadFile, - token: string, - passwordToken: string, - ): Promise { - try { - if (!token) { - throw Error(CustomError.TOKEN_MISSING); - } - const url = await apiURL("/public-collection/file"); - const response = await retryAsyncOperation( - () => - HTTPService.post(url, uploadFile, null, { - "X-Auth-Access-Token": token, - ...(passwordToken && { - "X-Auth-Access-Token-JWT": passwordToken, - }), - }), - handleUploadError, - ); - return response.data; - } catch (e) { - log.error("upload public File Failed", e); - throw e; - } - } - - /** - * Sibling of {@link fetchUploadURLs} for public albums. - */ - async fetchUploadURLs( - countHint: number, - credentials: PublicAlbumsCredentials, - ) { - const count = Math.min(50, countHint * 2).toString(); - const params = new URLSearchParams({ count }); - const url = await apiURL("/public-collection/upload-urls"); - const res = await fetch(`${url}?${params.toString()}`, { - headers: authenticatedPublicAlbumsRequestHeaders(credentials), - }); - ensureOk(res); - return ( - // TODO: The as cast will not be needed when tsc strict mode is - // enabled for this code. - ObjectUploadURLResponse.parse(await res.json()) - .urls as ObjectUploadURL[] - ); - } - - async fetchMultipartUploadURLs( - count: number, - token: string, - passwordToken: string, - ): Promise { - try { - if (!token) { - throw Error(CustomError.TOKEN_MISSING); - } - const response = await HTTPService.get( - await apiURL("/public-collection/multipart-upload-urls"), - { count }, - { - "X-Auth-Access-Token": token, - ...(passwordToken && { - "X-Auth-Access-Token-JWT": passwordToken, - }), - }, - ); - - return response.data.urls; - } catch (e) { - log.error("fetch public multipart-upload-url failed", e); - throw e; - } - } -} diff --git a/web/apps/photos/src/services/userService.ts b/web/apps/photos/src/services/userService.ts deleted file mode 100644 index 2665996f1d..0000000000 --- a/web/apps/photos/src/services/userService.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { HttpStatusCode } from "axios"; -import { putAttributes } from "ente-accounts/services/user"; -import log from "ente-base/log"; -import { apiURL } from "ente-base/origins"; -import type { UserDetails } from "ente-new/photos/services/user-details"; -import { ApiError } from "ente-shared/error"; -import HTTPService from "ente-shared/network/HTTPService"; -import { getData } from "ente-shared/storage/localStorage"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; - -const HAS_SET_KEYS = "hasSetKeys"; - -export const getPublicKey = async (email: string) => { - const token = getToken(); - - const resp = await HTTPService.get( - await apiURL("/users/public-key"), - { email }, - { "X-Auth-Token": token }, - ); - return resp.data.publicKey; -}; - -export const isTokenValid = async (token: string) => { - try { - const resp = await HTTPService.get( - await apiURL("/users/session-validity/v2"), - null, - { "X-Auth-Token": token }, - ); - try { - if (resp.data[HAS_SET_KEYS] === undefined) { - throw Error("resp.data.hasSetKey undefined"); - } - if (!resp.data.hasSetKeys) { - try { - await putAttributes( - token, - getData("originalKeyAttributes"), - ); - } catch (e) { - log.error("put attribute failed", e); - } - } - } catch (e) { - log.error("hasSetKeys not set in session validity response", e); - } - return true; - } catch (e) { - log.error("session-validity api call failed", e); - if ( - e instanceof ApiError && - e.httpStatusCode === HttpStatusCode.Unauthorized - ) { - return false; - } else { - return true; - } - } -}; - -export const getUserDetailsV2 = async (): Promise => { - try { - const token = getToken(); - - const resp = await HTTPService.get( - await apiURL("/users/details/v2"), - null, - { "X-Auth-Token": token }, - ); - return resp.data; - } catch (e) { - log.error("failed to get user details v2", e); - throw e; - } -}; diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index 3088158167..904e35e236 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -13,17 +13,15 @@ import type { FolderWatchSyncedFile, } from "ente-base/types/ipc"; import { type UploadResult } from "ente-gallery/services/upload"; -import type { Collection } from "ente-media/collection"; +import type { UploadAsset } from "ente-gallery/services/upload/upload-service"; import { EncryptedEnteFile } from "ente-media/file"; import { getLocalFiles, groupFilesByCollectionID, } from "ente-new/photos/services/files"; import { ensureString } from "ente-utils/ensure"; -import uploadManager, { - type UploadItemWithCollection, -} from "services/upload/uploadManager"; import { removeFromCollection } from "./collectionService"; +import { type UploadItemWithCollection, uploadManager } from "./upload-manager"; /** * Watch for file system folders and automatically update the corresponding Ente @@ -372,21 +370,17 @@ class FolderWatcher { * Callback invoked by the uploader whenever all the files we requested to * {@link upload} get uploaded. */ - async allFileUploadsDone( - uploadItemsWithCollection: UploadItemWithCollection[], - collections: Collection[], - ) { + async allFileUploadsDone(uploadedItems: UploadAsset[]) { const electron = ensureElectron(); const watch = this.activeWatch; log.debug(() => [ "watch/allFileUploadsDone", - JSON.stringify({ uploadItemsWithCollection, collections, watch }), + JSON.stringify({ uploadedItems, watch }), ]); - const { syncedFiles, ignoredFiles } = this.deduceSyncedAndIgnored( - uploadItemsWithCollection, - ); + const { syncedFiles, ignoredFiles } = + this.deduceSyncedAndIgnored(uploadedItems); if (syncedFiles.length > 0) await electron.watch.updateSyncedFiles( @@ -406,9 +400,7 @@ class FolderWatcher { this.debouncedRunNextEvent(); } - private deduceSyncedAndIgnored( - uploadItemsWithCollection: UploadItemWithCollection[], - ) { + private deduceSyncedAndIgnored(uploadedItems: UploadAsset[]) { const syncedFiles: FolderWatch["syncedFiles"] = []; const ignoredFiles: FolderWatch["ignoredFiles"] = []; @@ -427,7 +419,7 @@ class FolderWatcher { this.unUploadableFilePaths.delete(path); }; - for (const item of uploadItemsWithCollection) { + for (const item of uploadedItems) { // Re the usage of ensureString: For desktop watch, the only // possibility for a UploadItem is for it to be a string (the // absolute path to a file on disk). diff --git a/web/apps/photos/src/styles/photoswipe.css b/web/apps/photos/src/styles/photoswipe.css index 919011e56d..88e6771479 100644 --- a/web/apps/photos/src/styles/photoswipe.css +++ b/web/apps/photos/src/styles/photoswipe.css @@ -155,19 +155,19 @@ bottom: 0px; right: 0; margin: 20px 24px; - padding-inline: 16px; border-radius: 3px; /* Same opacity as the other controls. */ color: rgb(255 255 255 / 0.85); background-color: rgb(0 0 0 / 0.2); backdrop-filter: blur(10px); - text-align: right; max-width: 375px; max-height: 200px; p { + margin: 12px 17px; /* 4 lines max, ellipsis on overflow. */ word-break: break-word; overflow: hidden; + text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; diff --git a/web/apps/photos/src/types/gallery/index.ts b/web/apps/photos/src/types/gallery/index.ts index 67371e047d..bb23e2f2d3 100644 --- a/web/apps/photos/src/types/gallery/index.ts +++ b/web/apps/photos/src/types/gallery/index.ts @@ -1,7 +1,7 @@ import { TimeStampListItem } from "components/FileList"; import { FilesDownloadProgressAttributes } from "components/FilesDownloadProgress"; +import type { User } from "ente-accounts/services/user"; import { type SelectionContext } from "ente-new/photos/components/gallery"; -import type { User } from "ente-shared/user/types"; export interface SelectedState { [k: number]: boolean; diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index 5e1b46681c..78583ce53f 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -1,11 +1,10 @@ +import type { User } from "ente-accounts/services/user"; import { ensureElectron } from "ente-base/electron"; import { joinPath } from "ente-base/file-name"; import log from "ente-base/log"; -import { updateMagicMetadata } from "ente-gallery/services/magic-metadata"; import { type Collection, - CollectionMagicMetadataProps, - CollectionPublicMagicMetadataProps, + type CollectionOrder, CollectionSubType, } from "ente-media/collection"; import { EnteFile } from "ente-media/file"; @@ -14,11 +13,15 @@ import { DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME, HIDDEN_ITEMS_SECTION, addToCollection, + collection1To2, findDefaultHiddenCollectionIDs, isHiddenCollection, isIncomingShare, moveToCollection, restoreToCollection, + updateCollectionOrder, + updateCollectionSortOrder, + updateCollectionVisibility, } from "ente-new/photos/services/collection"; import { getAllLocalCollections, @@ -30,15 +33,10 @@ import { } from "ente-new/photos/services/files"; import { safeDirectoryName } from "ente-new/photos/utils/native-fs"; import { getData } from "ente-shared/storage/localStorage"; -import type { User } from "ente-shared/user/types"; -import { t } from "i18next"; import { createAlbum, removeFromCollection, unhideToCollection, - updateCollectionMagicMetadata, - updatePublicCollectionMagicMetadata, - updateSharedCollectionMagicMetadata, } from "services/collectionService"; import { SetFilesDownloadProgressAttributes, @@ -183,117 +181,20 @@ async function createCollectionDownloadFolder( return collectionDownloadPath; } -const _intSelectOption = (i: number) => { - const label = i === 0 ? t("none") : i.toString(); - return { label, value: i }; -}; - -export function getDeviceLimitOptions() { - return [0, 2, 5, 10, 25, 50].map((i) => _intSelectOption(i)); -} - export const changeCollectionVisibility = async ( collection: Collection, visibility: ItemVisibility, -) => { - try { - const updatedMagicMetadataProps: CollectionMagicMetadataProps = { - visibility, - }; +) => updateCollectionVisibility(await collection1To2(collection), visibility); - const user: User = getData("user"); - if (collection.owner.id === user.id) { - const updatedMagicMetadata = await updateMagicMetadata( - updatedMagicMetadataProps, - collection.magicMetadata, - collection.key, - ); - - await updateCollectionMagicMetadata( - collection, - updatedMagicMetadata, - ); - } else { - const updatedMagicMetadata = await updateMagicMetadata( - updatedMagicMetadataProps, - collection.sharedMagicMetadata, - collection.key, - ); - await updateSharedCollectionMagicMetadata( - collection, - updatedMagicMetadata, - ); - } - } catch (e) { - log.error("change collection visibility failed", e); - throw e; - } -}; +export const changeCollectionOrder = async ( + collection: Collection, + order: CollectionOrder, +) => updateCollectionOrder(await collection1To2(collection), order); export const changeCollectionSortOrder = async ( collection: Collection, asc: boolean, -) => { - try { - const updatedPublicMagicMetadataProps: CollectionPublicMagicMetadataProps = - { asc }; - - const updatedPubMagicMetadata = await updateMagicMetadata( - updatedPublicMagicMetadataProps, - collection.pubMagicMetadata, - collection.key, - ); - - await updatePublicCollectionMagicMetadata( - collection, - updatedPubMagicMetadata, - ); - } catch (e) { - log.error("change collection sort order failed", e); - } -}; - -export const changeCollectionOrder = async ( - collection: Collection, - order: number, -) => { - try { - const updatedMagicMetadataProps: CollectionMagicMetadataProps = { - order, - }; - - const updatedMagicMetadata = await updateMagicMetadata( - updatedMagicMetadataProps, - collection.magicMetadata, - collection.key, - ); - - await updateCollectionMagicMetadata(collection, updatedMagicMetadata); - } catch (e) { - log.error("change collection order failed", e); - } -}; - -export const changeCollectionSubType = async ( - collection: Collection, - subType: CollectionSubType, -) => { - try { - const updatedMagicMetadataProps: CollectionMagicMetadataProps = { - subType: subType, - }; - - const updatedMagicMetadata = await updateMagicMetadata( - updatedMagicMetadataProps, - collection.magicMetadata, - collection.key, - ); - await updateCollectionMagicMetadata(collection, updatedMagicMetadata); - } catch (e) { - log.error("change collection subType failed", e); - throw e; - } -}; +) => updateCollectionSortOrder(await collection1To2(collection), asc); export const getUserOwnedCollections = (collections: Collection[]) => { const user: User = getData("user"); @@ -303,7 +204,7 @@ export const getUserOwnedCollections = (collections: Collection[]) => { return collections.filter((collection) => collection.owner.id === user.id); }; -export const isQuickLinkCollection = (collection: Collection) => +const isQuickLinkCollection = (collection: Collection) => collection.magicMetadata?.data.subType == CollectionSubType.quicklink; export function isIncomingViewerShare(collection: Collection, user: User) { diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 005c815986..a3afa279a3 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,3 +1,4 @@ +import type { User } from "ente-accounts/services/user"; import { joinPath } from "ente-base/file-name"; import log from "ente-base/log"; import { type Electron } from "ente-base/types/ipc"; @@ -21,7 +22,7 @@ import { } from "ente-new/photos/services/collection"; import { safeFileName } from "ente-new/photos/utils/native-fs"; import { getData } from "ente-shared/storage/localStorage"; -import type { User } from "ente-shared/user/types"; +import { wait } from "ente-utils/promise"; import { t } from "i18next"; import { addMultipleToFavorites, @@ -60,6 +61,9 @@ export async function downloadFile(file: EnteFile) { new Blob([videoData], { type: videoType.mimeType }), ); downloadAndRevokeObjectURL(tempImageURL, imageFileName); + // Downloading multiple works everywhere except, you guessed it, + // Safari. Make up for their incompetence by adding a setTimeout. + await wait(300) /* arbitrary constant, 300ms */; downloadAndRevokeObjectURL(tempVideoURL, videoFileName); } else { const fileType = await detectFileTypeInfo( @@ -338,26 +342,6 @@ export const getUserOwnedFiles = (files: EnteFile[]) => { return files.filter((file) => file.ownerID === user.id); }; -export function getPersonalFiles( - files: EnteFile[], - user: User, - collectionIdToOwnerIDMap?: Map, -) { - if (!user?.id) { - throw Error("user missing"); - } - return files.filter( - (file) => - file.ownerID === user.id && - (!collectionIdToOwnerIDMap || - collectionIdToOwnerIDMap.get(file.collectionID) === user.id), - ); -} - -export function getIDBasedSortedFiles(files: EnteFile[]) { - return files.sort((a, b) => a.id - b.id); -} - export const shouldShowAvatar = (file: EnteFile, user: User) => { if (!file || !user) { return false; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index a9347f092f..ceb31acbc7 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -1,19 +1,19 @@ /* eslint-disable @typescript-eslint/dot-notation */ +import { + parseDateFromDigitGroups, + tryParseEpochMicrosecondsFromFileName, +} from "ente-gallery/services/upload/date"; +import { + matchJSONMetadata, + metadataJSONMapKeyForJSON, +} from "ente-gallery/services/upload/metadata-json"; import { FileType } from "ente-media/file-type"; import { getLocalCollections } from "ente-new/photos/services/collections"; import { getLocalFiles, groupFilesByCollectionID, } from "ente-new/photos/services/files"; -import { - parseDateFromDigitGroups, - tryParseEpochMicrosecondsFromFileName, -} from "services/upload/date"; -import { - matchTakeoutMetadata, - metadataJSONMapKeyForJSON, -} from "services/upload/takeout"; -import { getUserDetailsV2 } from "services/userService"; +import { getUserDetailsV2 } from "ente-new/photos/services/user-details"; const DATE_TIME_PARSING_TEST_FILE_NAMES = [ { @@ -447,14 +447,14 @@ function parseDateTimeFromFileNameTest() { const fileNameToJSONMappingTests = () => { for (const { filename, jsonFilename } of fileNameToJSONMappingCases) { - const jsonKey = metadataJSONMapKeyForJSON(0, jsonFilename); + const jsonKey = metadataJSONMapKeyForJSON(undefined, 0, jsonFilename); // See the docs for the file name matcher as to why it doesn't return // the key but instead indexes into the map for us. To test it, we // construct a placeholder map with a dummy entry for the expected key. const map = new Map([[jsonKey, {}]]); - if (!matchTakeoutMetadata(filename, 0, map)) { + if (!matchJSONMetadata(undefined, 0, filename, map)) { throw Error( `fileNameToJSONMappingTests failed ❌ for ${filename} and ${jsonFilename}`, ); diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index 92cf8acd2d..5c427dbef8 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -202,20 +202,26 @@ via [@fontsource-variable/inter](https://fontsource.org/fonts/inter/install). - [debounce](https://github.com/sindresorhus/debounce) and its promise-supporting sibling - [pDebounce](https://github.com/sindresorhus/p-debounce) are used for + [p-debounce](https://github.com/sindresorhus/p-debounce) are used for debouncing operations (See also: `[Note: Throttle and debounce]`). +- [bip39](https://github.com/bitcoinjs/bip39) is used for generating the 24-word + recovery key mnemonic. + - [zxcvbn](https://github.com/dropbox/zxcvbn) is used for password strength estimation. +- [fast-srp-hap](https://github.com/homebridge/fast-srp) is used for the maths + underlying the SRP protocol. + ## Media -- [ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm) is used to run FFmpeg - in the browser using WebAssembly (Wasm). Note that this is substantially - slower than native ffmpeg (the desktop app can, and does, bundle the faster - native ffmpeg implementation too). +- [@ffmpeg/ffmpeg](https://github.com/ffmpegwasm/ffmpeg.wasm) is used to run + FFmpeg in the browser using WebAssembly (Wasm). Note that this is + substantially slower than native ffmpeg (the desktop app can, and does, bundle + the faster native ffmpeg implementation too). -- [ExifReader](https://github.com/mattiasw/ExifReader) is used for Exif parsing. +- [exifreader](https://github.com/mattiasw/ExifReader) is used for Exif parsing. - [jszip](https://github.com/Stuk/jszip) is used for reading zip files in the web code (Live photos are zip files under the hood). Note that the desktop app @@ -232,7 +238,7 @@ via [@fontsource-variable/inter](https://fontsource.org/fonts/inter/install). ## Photos app specific -- [PhotoSwipe](https://photoswipe.com) provides the base image viewer on top of +- [photoswipe](https://photoswipe.com) provides the base image viewer on top of which we've built our file viewer. - For streaming video (HLS), we use three libraries: @@ -264,8 +270,8 @@ via [@fontsource-variable/inter](https://fontsource.org/fonts/inter/install). - [chrono-node](https://github.com/wanasit/chrono) is used for parsing natural language queries into dates for showing search results. -- [matrix](https://github.com/mljs/matrix) is mathematical matrix abstraction by - the machine learning code. It is used alongwith +- [ml-matrix](https://github.com/mljs/matrix) is mathematical matrix abstraction + by the machine learning code. It is used alongwith [similarity-transformation](https://github.com/shaileshpandit/similarity-transformation-js) during face alignment. diff --git a/web/docs/webauthn-passkeys.md b/web/docs/webauthn-passkeys.md index 39cc57ad53..b994fc98e4 100644 --- a/web/docs/webauthn-passkeys.md +++ b/web/docs/webauthn-passkeys.md @@ -293,8 +293,7 @@ const { // ... twoFactorSessionID, passkeySessionID, -} = await loginViaSRP(srpAttributes, kek); -setIsFirstLogin(true); +} = await verifySRP(srpAttributes, kek); if (passkeySessionID) { // ... } diff --git a/web/package.json b/web/package.json index 05b9b0410e..255aee9987 100644 --- a/web/package.json +++ b/web/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "concurrently": "^9.1.2", - "eslint": "^9.25.1", + "eslint": "^9.28.0", "prettier": "^3.5.3", "typescript": "^5.8.3" }, diff --git a/web/packages/accounts/components/LoginComponents.tsx b/web/packages/accounts/components/LoginComponents.tsx index 9433914f49..fb70567e16 100644 --- a/web/packages/accounts/components/LoginComponents.tsx +++ b/web/packages/accounts/components/LoginComponents.tsx @@ -19,22 +19,40 @@ import { AccountsPageFooter, } from "./layouts/centered-paper"; -export const PasswordHeader: React.FC = ({ - children, -}) => { - return ( - - {t("password")} - {children} - - ); -}; +interface HeaderCaptionProps { + /** + * If specified, then a caption to display below the title (which is + * expected to be passed as the `children`). + * + * The components which use the {@link HeaderCaptionProps} that they'll have + * the same height irrespective of whether or not the caption is provided. + * This allows us to use this component to get a similar look across various + * pages in the login flow (some of which have a caption, some which not). + */ + caption?: string; +} -const PasskeyHeader: React.FC = ({ children }) => { +export const PasswordHeader: React.FC = (props) => ( + + {t("password")} + +); + +const PasskeyHeader: React.FC = (props) => ( + + {t("passkey")} + +); + +export const AccountsPageTitleWithCaption: React.FC< + React.PropsWithChildren +> = ({ caption, children }) => { return ( - {t("passkey")} - {children} + {children} + + {caption ?? ""} + ); }; @@ -123,7 +141,7 @@ export const VerifyingPasskey: React.FC = ({ return ( - {email ?? ""} + diff --git a/web/packages/accounts/components/LoginContents.tsx b/web/packages/accounts/components/LoginContents.tsx index ce8c7a41f0..eae1479346 100644 --- a/web/packages/accounts/components/LoginContents.tsx +++ b/web/packages/accounts/components/LoginContents.tsx @@ -1,19 +1,20 @@ -import { Input, Stack, Typography } from "@mui/material"; +import { Input, Stack, TextField, Typography } from "@mui/material"; +import { AccountsPageFooter } from "ente-accounts/components/layouts/centered-paper"; import { - AccountsPageFooter, - AccountsPageTitle, -} from "ente-accounts/components/layouts/centered-paper"; -import { getSRPAttributes } from "ente-accounts/services/srp-remote"; -import { sendOTT } from "ente-accounts/services/user"; + getSRPAttributes, + saveSRPAttributes, +} from "ente-accounts/services/srp"; +import { savePartialLocalUser, sendOTT } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; +import { LoadingButton } from "ente-base/components/mui/LoadingButton"; import { isMuseumHTTPError } from "ente-base/http"; import log from "ente-base/log"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; -import { setData, setLSUser } from "ente-shared/storage/localStorage"; +import { useFormik } from "formik"; import { t } from "i18next"; import { useRouter } from "next/router"; +import React, { useCallback } from "react"; +import { z } from "zod/v4"; +import { AccountsPageTitleWithCaption } from "./LoginComponents"; interface LoginContentsProps { /** Called when the user clicks the signup option instead. */ @@ -33,13 +34,9 @@ export const LoginContents: React.FC = ({ }) => { const router = useRouter(); - const loginUser: SingleInputFormProps["callback"] = async ( - email, - setFieldError, - ) => { - try { + const loginUser = useCallback( + async (email: string, setFieldError: (message: string) => void) => { const srpAttributes = await getSRPAttributes(email); - log.debug(() => ["srpAttributes", JSON.stringify(srpAttributes)]); if (!srpAttributes || srpAttributes.isEmailMFAEnabled) { try { await sendOTT(email, "login"); @@ -52,32 +49,73 @@ export const LoginContents: React.FC = ({ } throw e; } - await setLSUser({ email }); + savePartialLocalUser({ email }); void router.push("/verify"); } else { - await setLSUser({ email }); - setData("srpAttributes", srpAttributes); + savePartialLocalUser({ email }); + saveSRPAttributes(srpAttributes); void router.push("/credentials"); } - } catch (e) { - log.error("Login failed", e); - setFieldError(t("generic_error")); - } - }; + }, + [router], + ); + + const formik = useFormik({ + initialValues: { email: "" }, + onSubmit: async ({ email }, { setFieldError }) => { + const setEmailFieldError = (message: string) => + setFieldError("email", message); + + if (!email) { + setEmailFieldError(t("required")); + return; + } + + if (!z.email().safeParse(email).success) { + setEmailFieldError(t("invalid_email_error")); + return; + } + + try { + await loginUser(email, setEmailFieldError); + } catch (e) { + log.error("Failed to login", e); + setEmailFieldError(t("generic_error")); + } + }, + }); return ( <> - {t("login")} - - } - /> + + {t("login")} + +
+ + + + {t("login")} + + diff --git a/web/packages/accounts/components/NewPasswordForm.tsx b/web/packages/accounts/components/NewPasswordForm.tsx new file mode 100644 index 0000000000..a0be4565f9 --- /dev/null +++ b/web/packages/accounts/components/NewPasswordForm.tsx @@ -0,0 +1,163 @@ +import { Input, TextField, Typography } from "@mui/material"; +import { isWeakPassword } from "ente-accounts/utils/password"; +import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; +import log from "ente-base/log"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import { useCallback, useState } from "react"; +import { Trans } from "react-i18next"; +import { PasswordStrengthHint } from "./PasswordStrength"; + +export interface NewPasswordFormProps { + /** + * The email of the user whose password we are setting. + * + * This is used to show a hidden input field of type email (and the provided + * value) to aid password managers to detect and save the new password, + * associating it with the user's email. + */ + userEmail: string; + /** + * The title of the form's submit button. + */ + submitButtonTitle: string; + /** + * Submission handler. A callback invoked when the submit button is pressed. + * + * @param password The new password entered by the user. The form will first + * check that both of the passwords entered by the user match, and that the + * password is not too weak. + * + * @param setPasswordsFieldError A function that can be called to show an + * error message below the password fields. + */ + onSubmit: ( + password: string, + setPasswordsFieldError: (message: string) => void, + ) => Promise; +} + +/** + * A form showing two password input fields, a password strength indicator, and + * a submit button. + * + * This form can be used both for the initial setup of the password, and for + * later changing it. + */ +export const NewPasswordForm: React.FC = ({ + userEmail, + submitButtonTitle, + onSubmit, +}) => { + const [showPassword, setShowPassword] = useState(false); + + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); + + const formik = useFormik({ + initialValues: { password: "", confirmPassword: "" }, + onSubmit: async ({ password, confirmPassword }, { setFieldError }) => { + const setPasswordsFieldError = (message: string) => + setFieldError("confirmPassword", message); + + if (!confirmPassword) { + setPasswordsFieldError(t("required")); + return; + } + + if (password != confirmPassword) { + setPasswordsFieldError(t("password_mismatch_error")); + return; + } + + try { + await onSubmit(password, setPasswordsFieldError); + } catch (e) { + log.error("Could not set password", e); + setPasswordsFieldError(t("generic_error")); + } + }, + }); + + return ( +
+ + {t("pick_password_hint")} + + + + + ), + }, + }} + /> + + + + + + + + + {submitButtonTitle} + + ({ + textAlign: "center", + mt: 1, + color: "text.muted", + minHeight: theme.typography.small.lineHeight, + })} + > + {formik.isSubmitting ? t("key_generation_in_progress") : ""} + + + ); +}; diff --git a/web/packages/accounts/components/PasswordStrength.tsx b/web/packages/accounts/components/PasswordStrength.tsx index bbb6dcf347..5b106a9a05 100644 --- a/web/packages/accounts/components/PasswordStrength.tsx +++ b/web/packages/accounts/components/PasswordStrength.tsx @@ -28,6 +28,7 @@ export const PasswordStrengthHint: React.FC = ({ variant="small" sx={{ mt: "8px", + mx: "2px", alignSelf: "flex-start", whiteSpace: "pre", color: "var(--et-color)", @@ -35,7 +36,7 @@ export const PasswordStrengthHint: React.FC = ({ style={{ "--et-color": color } as React.CSSProperties} > {password - ? t("passphrase_strength", { context: passwordStrength }) + ? t("password_strength", { context: passwordStrength }) : /* empty space + white-space: pre to prevent layout shift. */ " "} diff --git a/web/packages/accounts/components/RecoveryKey.tsx b/web/packages/accounts/components/RecoveryKey.tsx index 45722ac170..525e1bc53c 100644 --- a/web/packages/accounts/components/RecoveryKey.tsx +++ b/web/packages/accounts/components/RecoveryKey.tsx @@ -6,7 +6,6 @@ import { Stack, Typography, } from "@mui/material"; -import * as bip39 from "bip39"; import { type MiniDialogAttributes } from "ente-base/components/MiniDialog"; import { SpacedRow } from "ente-base/components/containers"; import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton"; @@ -16,14 +15,14 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks"; import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; import log from "ente-base/log"; import { downloadString } from "ente-base/utils/web"; -import { getRecoveryKey } from "ente-shared/crypto/helpers"; import { t } from "i18next"; import { useCallback, useEffect, useState } from "react"; +import { + getUserRecoveryKey, + recoveryKeyToMnemonic, +} from "../services/recovery-key"; import { CodeBlock } from "./CodeBlock"; -// mobile client library only supports english. -bip39.setDefaultWordlist("english"); - type RecoveryKeyProps = ModalVisibilityProps & { showMiniDialog: (attributes: MiniDialogAttributes) => void; }; @@ -50,7 +49,7 @@ export const RecoveryKey: React.FC = ({ useEffect(() => { if (!open) return; - void getRecoveryKeyMnemonic() + void getUserRecoveryKeyMnemonic() .then((key) => setRecoveryKey(key)) .catch(handleLoadError); }, [open, handleLoadError]); @@ -115,8 +114,8 @@ export const RecoveryKey: React.FC = ({ ); }; -const getRecoveryKeyMnemonic = async () => - bip39.entropyToMnemonic(await getRecoveryKey()); +const getUserRecoveryKeyMnemonic = async () => + recoveryKeyToMnemonic(await getUserRecoveryKey()); const downloadRecoveryKeyMnemonic = (key: string) => downloadString(key, "ente-recovery-key.txt"); diff --git a/web/packages/accounts/components/SetPasswordForm.tsx b/web/packages/accounts/components/SetPasswordForm.tsx deleted file mode 100644 index 4c407b8137..0000000000 --- a/web/packages/accounts/components/SetPasswordForm.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { Box, Input, TextField, Typography } from "@mui/material"; -import { isWeakPassword } from "ente-accounts/utils/password"; -import { LoadingButton } from "ente-base/components/mui/LoadingButton"; -import ShowHidePassword from "ente-shared/components/Form/ShowHidePassword"; -import { Formik } from "formik"; -import { t } from "i18next"; -import React, { useState } from "react"; -import { Trans } from "react-i18next"; -import * as Yup from "yup"; -import { PasswordStrengthHint } from "./PasswordStrength"; - -export interface SetPasswordFormProps { - userEmail: string; - callback: ( - passphrase: string, - setFieldError: ( - field: keyof SetPasswordFormValues, - message: string, - ) => void, - ) => Promise; - buttonText: string; -} - -export interface SetPasswordFormValues { - passphrase: string; - confirm: string; -} - -function SetPasswordForm(props: SetPasswordFormProps) { - const [loading, setLoading] = useState(false); - const [showPassword, setShowPassword] = useState(false); - - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; - - const handleMouseDownPassword = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - }; - - const onSubmit = async ( - values: SetPasswordFormValues, - { - setFieldError, - }: { - setFieldError: ( - field: keyof SetPasswordFormValues, - message: string, - ) => void; - }, - ) => { - setLoading(true); - try { - const { passphrase, confirm } = values; - if (passphrase === confirm) { - await props.callback(passphrase, setFieldError); - } else { - setFieldError("confirm", t("password_mismatch_error")); - } - } catch (e) { - const message = e instanceof Error ? e.message : ""; - setFieldError("confirm", `${t("generic_error_retry")} ${message}`); - } finally { - setLoading(false); - } - }; - - return ( - - initialValues={{ passphrase: "", confirm: "" }} - validationSchema={Yup.object().shape({ - passphrase: Yup.string().required(t("required")), - confirm: Yup.string().required(t("required")), - })} - validateOnChange={false} - validateOnBlur={false} - onSubmit={onSubmit} - > - {({ values, errors, handleChange, handleSubmit }) => ( -
- - {t("pick_password_hint")} - - - - - ), - }, - }} - /> - - - - - - - - - - {props.buttonText} - - {loading && ( - - {t("key_generation_in_progress")} - - )} - - - )} - - ); -} -export default SetPasswordForm; diff --git a/web/packages/accounts/components/SignUpContents.tsx b/web/packages/accounts/components/SignUpContents.tsx index c06777668b..6b3d09d94e 100644 --- a/web/packages/accounts/components/SignUpContents.tsx +++ b/web/packages/accounts/components/SignUpContents.tsx @@ -1,56 +1,54 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import { - Box, Checkbox, Divider, FormControlLabel, FormGroup, IconButton, InputAdornment, + InputLabel, Link, Stack, TextField, Tooltip, Typography, } from "@mui/material"; -import { generateKeyAndSRPAttributes } from "ente-accounts/services/srp"; -import { sendOTT } from "ente-accounts/services/user"; +import { + generateSRPSetupAttributes, + stashSRPSetupAttributes, +} from "ente-accounts/services/srp"; +import { + generateAndSaveInteractiveKeyAttributes, + generateKeysAndAttributes, + savePartialLocalUser, + sendOTT, + type GenerateKeysAndAttributesResult, +} from "ente-accounts/services/user"; import { isWeakPassword } from "ente-accounts/utils/password"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; +import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types"; import { isMuseumHTTPError } from "ente-base/http"; import log from "ente-base/log"; -import { setLSUser } from "ente-shared//storage/localStorage"; -import { VerticallyCentered } from "ente-shared/components/Container"; -import ShowHidePassword from "ente-shared/components/Form/ShowHidePassword"; -import { - generateAndSaveIntermediateKeyAttributes, - saveKeyInSessionStore, -} from "ente-shared/crypto/helpers"; +import { saveMasterKeyInSessionAndSafeStore } from "ente-base/session"; import { setData } from "ente-shared/storage/localStorage"; import { setJustSignedUp, setLocalReferralSource, } from "ente-shared/storage/localStorage/helpers"; -import { Formik, type FormikHelpers } from "formik"; +import { useFormik } from "formik"; import { t } from "i18next"; import type { NextRouter } from "next/router"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { Trans } from "react-i18next"; -import * as Yup from "yup"; +import { z } from "zod/v4"; import { PasswordStrengthHint } from "./PasswordStrength"; import { AccountsPageFooter, AccountsPageTitle, } from "./layouts/centered-paper"; -interface FormValues { - email: string; - passphrase: string; - confirm: string; - referral: string; -} - interface SignUpContentsProps { router: NextRouter; /** Called when the user clicks the login option instead. */ @@ -64,277 +62,258 @@ export const SignUpContents: React.FC = ({ onLogin, host, }) => { - const [acceptTerms, setAcceptTerms] = useState(false); - const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); - const handleClickShowPassword = () => { - setShowPassword(!showPassword); - }; + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); - const handleMouseDownPassword = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - }; - - const registerUser = async ( - { email, passphrase, confirm, referral }: FormValues, - { setFieldError }: FormikHelpers, - ) => { - try { - if (passphrase !== confirm) { - setFieldError("confirm", t("password_mismatch_error")); + const formik = useFormik({ + initialValues: { + email: "", + password: "", + confirmPassword: "", + referral: "", + acceptedTerms: false, + }, + onSubmit: async ( + { email, password, confirmPassword, referral }, + { setFieldError }, + ) => { + if (!email) { + setFieldError("email", t("required")); return; } - setLoading(true); + + if (!z.email().safeParse(email).success) { + setFieldError("email", t("invalid_email_error")); + return; + } + + if (!password) { + setFieldError("password", t("required")); + return; + } + + if (!confirmPassword) { + setFieldError("confirmPassword", t("required")); + return; + } + + if (password != confirmPassword) { + setFieldError("confirmPassword", t("password_mismatch_error")); + return; + } + try { setLocalReferralSource(referral); - await sendOTT(email, "signup"); - await setLSUser({ email }); - } catch (e) { - log.error("Signup failed", e); - if ( - await isMuseumHTTPError(e, 409, "USER_ALREADY_REGISTERED") - ) { - setFieldError("email", t("email_already_registered")); - } else { - setFieldError("email", t("generic_error")); - } - throw e; - } - try { - const { keyAttributes, masterKey, srpSetupAttributes } = - await generateKeyAndSRPAttributes(passphrase); + try { + await sendOTT(email, "signup"); + } catch (e) { + if ( + await isMuseumHTTPError( + e, + 409, + "USER_ALREADY_REGISTERED", + ) + ) { + setFieldError("email", t("email_already_registered")); + return; + } + throw e; + } + + savePartialLocalUser({ email }); + + let gkResult: GenerateKeysAndAttributesResult; + try { + gkResult = await generateKeysAndAttributes(password); + } catch (e) { + if ( + e instanceof Error && + e.message == deriveKeyInsufficientMemoryErrorMessage + ) { + setFieldError( + "confirmPassword", + t("password_generation_failed"), + ); + return; + } + throw e; + } + + const { masterKey, kek, keyAttributes } = gkResult; setData("originalKeyAttributes", keyAttributes); - setData("srpSetupAttributes", srpSetupAttributes); - await generateAndSaveIntermediateKeyAttributes( - passphrase, + stashSRPSetupAttributes(await generateSRPSetupAttributes(kek)); + await generateAndSaveInteractiveKeyAttributes( + password, keyAttributes, masterKey, ); + await saveMasterKeyInSessionAndSafeStore(masterKey); - await saveKeyInSessionStore("encryptionKey", masterKey); setJustSignedUp(true); void router.push("/verify"); } catch (e) { - setFieldError("confirm", t("password_generation_failed")); - throw e; + log.error("Signup failed", e); + setFieldError("confirmPassword", t("generic_error")); } - } catch (e) { - log.error("signup failed", e); - } - setLoading(false); - }; + }, + }); const form = ( - - initialValues={{ - email: "", - passphrase: "", - confirm: "", - referral: "", - }} - validationSchema={Yup.object().shape({ - email: Yup.string() - .email(t("invalid_email_error")) - .required(t("required")), - passphrase: Yup.string().required(t("required")), - confirm: Yup.string().required(t("required")), - })} - validateOnChange={false} - validateOnBlur={false} - onSubmit={registerUser} - > - {({ - values, - errors, - handleChange, - handleSubmit, - }): React.JSX.Element => ( -
- - + + + ), + }, + }} + /> + + + + {t("referral_source_hint")} + + + + + + + + + ), + }, + }} + /> + + - - + ), - }, - }} - /> - - - - - - - {t("referral_source_hint")} - - - - - - - - - ), - }, - }} - /> - - - - setAcceptTerms(e.target.checked) - } - color="accent" - /> - } - label={ - - - ), - b: ( - - ), - }} + b: ( + - - } - /> - - - - - {t("create_account")} - - {loading && ( - - {t("key_generation_in_progress")} - - )} - - - )} - + /> + + } + /> + + + {t("create_account")} + + ({ + mt: 1, + textAlign: "center", + color: "text.muted", + // Prevent layout shift by using a minHeight equal to the + // lineHeight of the eventual content that'll be shown. + minHeight: theme.typography.small.lineHeight, + })} + > + {formik.isSubmitting ? t("key_generation_in_progress") : ""} + + ); return ( <> {t("sign_up")} {form} - + diff --git a/web/packages/accounts/components/VerifyMasterPasswordForm.tsx b/web/packages/accounts/components/VerifyMasterPasswordForm.tsx new file mode 100644 index 0000000000..73d0b529ba --- /dev/null +++ b/web/packages/accounts/components/VerifyMasterPasswordForm.tsx @@ -0,0 +1,236 @@ +import { Input, TextField } from "@mui/material"; +import { + srpVerificationUnauthorizedErrorMessage, + type SRPAttributes, +} from "ente-accounts/services/srp"; +import type { KeyAttributes, User } from "ente-accounts/services/user"; +import { LoadingButton } from "ente-base/components/mui/LoadingButton"; +import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; +import { sharedCryptoWorker } from "ente-base/crypto"; +import log from "ente-base/log"; +import { CustomError } from "ente-shared/error"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import { useCallback, useState } from "react"; + +export interface VerifyMasterPasswordFormProps { + /** + * The user whose password we're trying to verify. + */ + user: User | undefined; + /** + * The user's key attributes. + */ + keyAttributes: KeyAttributes | undefined; + /** + * A callback invoked when the form wants to get {@link KeyAttributes}. + * + * It is only provided during the login flow, where we do not have + * {@link keyAttributes} already available for the user. In case the form is + * used for reauthenticating the user after they've already logged in, then + * this function will not be provided. + * + * This function can throw an `CustomError.TWO_FACTOR_ENABLED` to signal to + * the form that some other form of second factor is enabled and the user + * has been redirected to a two factor verification page. + * + * @throws A Error with message + * {@link srpVerificationUnauthorizedErrorMessage} to signal that either + * that the password is incorrect, or no account with the provided email + * exists. + */ + getKeyAttributes?: (kek: string) => Promise; + /** + * The user's SRP attributes. + */ + srpAttributes?: SRPAttributes; + /** + * The title of the submit button no the form. + */ + submitButtonTitle: string; + /** + * The callback invoked with the verified password, and all the other + * auxillary information that was ascertained when verifying it. + * + * @param key The user's master key obtained after decrypting it by using + * the KEK derived from their password. + * + * @param kek The key used for encrypting the user's master key. + * + * @param keyAttributes The user's key attributes (either those that we + * started with, or those that we fetched on the way using + * {@link getKeyAttributes}). + * + * @param password The plaintext password. This can be used during login to + * derive another encrypted key using interactive mem/ops limits for faster + * reauthentication after the initial login. + */ + onVerify: ( + key: string, + kek: string, + keyAttributes: KeyAttributes, + password: string, + ) => void; +} + +/** + * A form with a text field that can be used to ask the user to verify their + * password. + */ +export const VerifyMasterPasswordForm: React.FC< + VerifyMasterPasswordFormProps +> = ({ + user, + keyAttributes, + srpAttributes, + getKeyAttributes, + onVerify, + submitButtonTitle, +}) => { + const [showPassword, setShowPassword] = useState(false); + + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); + + const formik = useFormik({ + initialValues: { password: "" }, + onSubmit: async ({ password }, { setFieldError }) => { + const setPasswordFieldError = (message: string) => + setFieldError("password", message); + + if (!password) { + setPasswordFieldError(t("required")); + return; + } + + try { + await verifyPassword(password, setPasswordFieldError); + } catch (e) { + log.error("Failed to to verify password", e); + setPasswordFieldError(t("generic_error")); + } + }, + }); + + const verifyPassword = async ( + password: string, + setFieldError: (message: string) => void, + ) => { + const cryptoWorker = await sharedCryptoWorker(); + let kek: string; + if (srpAttributes) { + try { + kek = await cryptoWorker.deriveKey( + password, + srpAttributes.kekSalt, + srpAttributes.opsLimit, + srpAttributes.memLimit, + ); + } catch (e) { + log.error("Failed to derive kek", e); + setFieldError(t("weak_device_hint")); + return; + } + } else if (keyAttributes) { + try { + kek = await cryptoWorker.deriveKey( + password, + keyAttributes.kekSalt, + keyAttributes.opsLimit, + keyAttributes.memLimit, + ); + } catch (e) { + log.error("Failed to derive kek", e); + setFieldError(t("weak_device_hint")); + return; + } + } else throw new Error("Both SRP and key attributes are missing"); + + if (!keyAttributes && typeof getKeyAttributes == "function") { + try { + keyAttributes = await getKeyAttributes(kek); + } catch (e) { + if (e instanceof Error) { + switch (e.message) { + case CustomError.TWO_FACTOR_ENABLED: + // Two factor enabled, user has been redirected to + // the two-factor verification page. + return; + + case srpVerificationUnauthorizedErrorMessage: + log.error("Incorrect password or no account", e); + setFieldError( + t("incorrect_password_or_no_account"), + ); + return; + } + } + throw e; + } + } + + if (!keyAttributes) throw Error("Couldn't get key attributes"); + + let key: string; + try { + key = await cryptoWorker.decryptBox( + { + encryptedData: keyAttributes.encryptedKey, + nonce: keyAttributes.keyDecryptionNonce, + }, + kek, + ); + } catch { + setFieldError(t("incorrect_password")); + return; + } + + onVerify(key, kek, keyAttributes, password); + }; + + return ( +
+ + + ), + }, + }} + /> + + {submitButtonTitle} + + + ); +}; diff --git a/web/packages/accounts/components/utils/second-factor-choice.ts b/web/packages/accounts/components/utils/second-factor-choice.ts index e263bb6af3..1557255ae3 100644 --- a/web/packages/accounts/components/utils/second-factor-choice.ts +++ b/web/packages/accounts/components/utils/second-factor-choice.ts @@ -3,9 +3,9 @@ * needs to be in a separate file to allow fast refresh. */ +import type { EmailOrSRPVerificationResponse } from "ente-accounts/services/user"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useCallback, useMemo, useRef } from "react"; -import type { UserVerificationResponse } from "../../services/user"; import type { SecondFactorType } from "../SecondFactorChoice"; /** @@ -39,7 +39,7 @@ export const useSecondFactorChoiceIfNeeded = () => { ); const userVerificationResultAfterResolvingSecondFactorChoice = useCallback( - async (response: UserVerificationResponse) => { + async (response: EmailOrSRPVerificationResponse) => { const { twoFactorSessionID: _twoFactorSessionIDV1, twoFactorSessionIDV2: _twoFactorSessionIDV2, diff --git a/web/packages/accounts/eslint.config.mjs b/web/packages/accounts/eslint.config.mjs index 3809e67788..f5408898a9 100644 --- a/web/packages/accounts/eslint.config.mjs +++ b/web/packages/accounts/eslint.config.mjs @@ -7,11 +7,9 @@ export default [ /* TODO: */ "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-call": "off", }, }, ]; diff --git a/web/packages/accounts/package.json b/web/packages/accounts/package.json index d80a131352..33476d9798 100644 --- a/web/packages/accounts/package.json +++ b/web/packages/accounts/package.json @@ -3,7 +3,6 @@ "version": "0.0.0", "private": true, "dependencies": { - "@types/zxcvbn": "^4.4.5", "bip39": "^3.1.0", "ente-base": "*", "ente-shared": "*", @@ -15,8 +14,9 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/react": "^19.1.7", + "@types/react-dom": "^19.1.6", + "@types/zxcvbn": "^4.4.5", "ente-build-config": "*" } } diff --git a/web/packages/accounts/pages/change-email.tsx b/web/packages/accounts/pages/change-email.tsx index 4aa43396d8..058c3abb07 100644 --- a/web/packages/accounts/pages/change-email.tsx +++ b/web/packages/accounts/pages/change-email.tsx @@ -1,33 +1,26 @@ +import CheckIcon from "@mui/icons-material/Check"; import { Alert, Box, TextField } from "@mui/material"; import { AccountsPageContents, AccountsPageFooter, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; +import { useRedirectIfNeedsCredentials } from "ente-accounts/components/utils/use-redirect"; import { appHomeRoute } from "ente-accounts/services/redirect"; import { changeEmail, sendOTT } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingButton } from "ente-base/components/mui/LoadingButton"; import { isHTTPErrorWithStatus } from "ente-base/http"; import log from "ente-base/log"; -import { VerticallyCentered } from "ente-shared/components/Container"; -import { getData, setLSUser } from "ente-shared/storage/localStorage"; -import { Formik, type FormikHelpers } from "formik"; +import { useFormik } from "formik"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { Trans } from "react-i18next"; -import * as Yup from "yup"; +import { z } from "zod/v4"; const Page: React.FC = () => { - const router = useRouter(); - - useEffect(() => { - const user = getData("user"); - if (!user?.token) { - void router.push("/"); - } - }, [router]); + useRedirectIfNeedsCredentials("/change-email"); return ( @@ -39,161 +32,147 @@ const Page: React.FC = () => { export default Page; -interface formValues { - email: string; - ott?: string; -} - const ChangeEmailForm: React.FC = () => { - const [loading, setLoading] = useState(false); - const [ottInputVisible, setShowOttInputVisibility] = useState(false); - const [email, setEmail] = useState(null); - const [showMessage, setShowMessage] = useState(false); + const [requestedEmail, setRequestedEmail] = useState(""); + const [showSentConfirmation, setShowSentConfirmation] = useState(false); const router = useRouter(); - const requestOTT = async ( - { email }: formValues, - { setFieldError }: FormikHelpers, - ) => { - try { - setLoading(true); - await sendOTT(email, "change"); - setEmail(email); - setShowOttInputVisibility(true); - setShowMessage(true); - // TODO: What was this meant to focus on? The ref referred to an - // Form element that was removed. Is this still needed. - // setTimeout(() => { - // ottInputRef.current?.focus(); - // }, 250); - } catch (e) { - log.error(e); - setFieldError( - "email", - isHTTPErrorWithStatus(e, 403) - ? t("email_already_taken") - : t("generic_error"), - ); - } - setLoading(false); - }; + const redirectToAppHome = useCallback(() => { + void router.push(appHomeRoute); + }, [router]); - const requestEmailChange = async ( - { email, ott }: formValues, - { setFieldError }: FormikHelpers, - ) => { - try { - setLoading(true); - await changeEmail(email, ott!); - await setLSUser({ ...getData("user"), email }); - setLoading(false); - void goToApp(); - } catch (e) { - log.error(e); - setLoading(false); - setFieldError("ott", t("incorrect_code")); - } - }; + const formik = useFormik({ + initialValues: { email: "", ott: "" }, + onSubmit: async ({ email, ott }, { setFieldError }) => { + if (!email) { + setFieldError("email", t("required")); + return; + } - const goToApp = () => router.push(appHomeRoute); + if (!z.email().safeParse(email).success) { + setFieldError("email", t("invalid_email_error")); + return; + } + + if (!requestedEmail) { + try { + await sendOTT(email, "change"); + } catch (e) { + log.error("Could not send OTT for email change", e); + setFieldError( + "email", + isHTTPErrorWithStatus(e, 403) + ? t("email_already_taken") + : t("generic_error"), + ); + return; + } + + setRequestedEmail(email); + setShowSentConfirmation(true); + } else { + if (!ott) { + setFieldError("ott", t("required")); + return; + } + + try { + await changeEmail(email, ott); + } catch (e) { + log.error("Could not change email", e); + setFieldError( + "ott", + isHTTPErrorWithStatus(e, 401) + ? t("incorrect_code") + : isHTTPErrorWithStatus(e, 410) + ? t("expired_code_error") + : t("generic_error"), + ); + return; + } + + redirectToAppHome(); + } + }, + }); return ( - - initialValues={{ email: "" }} - validationSchema={ - ottInputVisible - ? Yup.object().shape({ - email: Yup.string() - .email(t("invalid_email_error")) - .required(t("required")), - ott: Yup.string().required(t("required")), - }) - : Yup.object().shape({ - email: Yup.string() - .email(t("invalid_email_error")) - .required(t("required")), - }) - } - validateOnChange={false} - validateOnBlur={false} - onSubmit={!ottInputVisible ? requestOTT : requestEmailChange} - > - {({ values, errors, handleChange, handleSubmit }) => ( - <> - {showMessage && ( - setShowMessage(false)} - > - - ), - }} - values={{ email }} - /> - - )} -
- - - {ottInputVisible && ( - + {requestedEmail && showSentConfirmation && ( + } + severity="success" + onClose={() => setShowSentConfirmation(false)} + > + - )} - - {!ottInputVisible ? t("send_otp") : t("verify")} - - - - - - {ottInputVisible && ( - setShowOttInputVisibility(false)} - > - {t("change_email")}? - - )} - - {t("go_back")} - - - + ), + }} + values={{ email: requestedEmail }} + /> + )} - +
+ + {requestedEmail && ( + + )} + + {!requestedEmail ? t("send_otp") : t("verify")} + + + + {requestedEmail && ( + setRequestedEmail("")}> + {t("change_email")}? + + )} + + {t("go_back")} + + + ); }; diff --git a/web/packages/accounts/pages/change-password.tsx b/web/packages/accounts/pages/change-password.tsx index c39c0daa2f..eb159040b4 100644 --- a/web/packages/accounts/pages/change-password.tsx +++ b/web/packages/accounts/pages/change-password.tsx @@ -1,155 +1,97 @@ +import { Divider } from "@mui/material"; import { AccountsPageContents, AccountsPageFooter, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; -import SetPasswordForm, { - type SetPasswordFormProps, -} from "ente-accounts/components/SetPasswordForm"; import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect"; import { - convertBase64ToBuffer, - convertBufferToBase64, - generateSRPClient, - generateSRPSetupAttributes, -} from "ente-accounts/services/srp"; -import { - getSRPAttributes, - startSRPSetup, - updateSRPAndKeys, -} from "ente-accounts/services/srp-remote"; -import type { UpdatedKey } from "ente-accounts/services/user"; + changePassword, + localUser, + type LocalUser, +} from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; -import { sharedCryptoWorker } from "ente-base/crypto"; -import { - generateAndSaveIntermediateKeyAttributes, - generateLoginSubKey, - saveKeyInSessionStore, -} from "ente-shared/crypto/helpers"; +import { LoadingIndicator } from "ente-base/components/loaders"; +import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types"; +import log from "ente-base/log"; import { getData, setData } from "ente-shared/storage/localStorage"; -import { getActualKey } from "ente-shared/user"; -import type { KEK, KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { + NewPasswordForm, + type NewPasswordFormProps, +} from "../components/NewPasswordForm"; +/** + * A page that allows a user to reset or change their password. + */ const Page: React.FC = () => { - const [token, setToken] = useState(); - const [user, setUser] = useState(); + const [user, setUser] = useState(); const router = useRouter(); useEffect(() => { - const user = getData("user"); - setUser(user); - if (!user?.token) { + const user = localUser(); + if (user) { + setUser(user); + } else { stashRedirect("/change-password"); void router.push("/"); - } else { - setToken(user.token); } }, [router]); - const onSubmit: SetPasswordFormProps["callback"] = async ( - passphrase, - setFieldError, - ) => { - const cryptoWorker = await sharedCryptoWorker(); - const key = await getActualKey(); - const keyAttributes: KeyAttributes = getData("keyAttributes"); - const kekSalt = await cryptoWorker.generateSaltToDeriveKey(); - let kek: KEK; - try { - kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt); - } catch { - setFieldError("confirm", t("password_generation_failed")); - return; - } - const encryptedKeyAttributes = await cryptoWorker.encryptToB64( - key, - kek.key, - ); - const updatedKey: UpdatedKey = { - kekSalt, - encryptedKey: encryptedKeyAttributes.encryptedData, - keyDecryptionNonce: encryptedKeyAttributes.nonce, - opsLimit: kek.opsLimit, - memLimit: kek.memLimit, - }; + return user ? : ; +}; - const loginSubKey = await generateLoginSubKey(kek.key); +export default Page; - const { srpUserID, srpSalt, srpVerifier } = - await generateSRPSetupAttributes(loginSubKey); +interface PageContentsProps { + user: LocalUser; +} - const srpClient = await generateSRPClient( - srpSalt, - srpUserID, - loginSubKey, - ); +const PageContents: React.FC = ({ user }) => { + const router = useRouter(); - const srpA = convertBufferToBase64(srpClient.computeA()); - - const { setupID, srpB } = await startSRPSetup(token!, { - srpUserID, - srpSalt, - srpVerifier, - srpA, - }); - - srpClient.setB(convertBase64ToBuffer(srpB)); - - const srpM1 = convertBufferToBase64(srpClient.computeM1()); - - await updateSRPAndKeys(token!, { - setupID, - srpM1, - updatedKeyAttr: updatedKey, - }); - - // Update the SRP attributes that are stored locally. - if (user?.email) { - const srpAttributes = await getSRPAttributes(user.email); - if (srpAttributes) { - setData("srpAttributes", srpAttributes); - } - } - - const updatedKeyAttributes = Object.assign(keyAttributes, updatedKey); - await generateAndSaveIntermediateKeyAttributes( - passphrase, - updatedKeyAttributes, - key, - ); - - await saveKeyInSessionStore("encryptionKey", key); - - redirectToAppHome(); - }; - - const redirectToAppHome = () => { + const redirectToAppHome = useCallback(() => { setData("showBackButton", { value: true }); void router.push(appHomeRoute); - }; + }, [router]); + + const handleSubmit: NewPasswordFormProps["onSubmit"] = async ( + password, + setPasswordsFieldError, + ) => + changePassword(password) + .then(redirectToAppHome) + .catch((e: unknown) => { + log.error("Could not change password", e); + setPasswordsFieldError( + e instanceof Error && + e.message == deriveKeyInsufficientMemoryErrorMessage + ? t("password_generation_failed") + : t("generic_error"), + ); + }); - // TODO: Handle the case where user is not loaded yet. return ( {t("change_password")} - {(getData("showBackButton")?.value ?? true) && ( - - - {t("go_back")} - - + <> + + + + {t("go_back")} + + + )} ); }; - -export default Page; diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index c0f9013394..c03b269ae0 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -7,6 +7,10 @@ import { import { SecondFactorChoice } from "ente-accounts/components/SecondFactorChoice"; import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog"; import { useSecondFactorChoiceIfNeeded } from "ente-accounts/components/utils/second-factor-choice"; +import { + VerifyMasterPasswordForm, + type VerifyMasterPasswordFormProps, +} from "ente-accounts/components/VerifyMasterPasswordForm"; import { openPasskeyVerificationURL, passkeyVerificationRedirectURL, @@ -17,29 +21,32 @@ import { unstashRedirect, } from "ente-accounts/services/redirect"; import { checkSessionValidity } from "ente-accounts/services/session"; +import type { SRPAttributes } from "ente-accounts/services/srp"; import { - configureSRP, generateSRPSetupAttributes, - loginViaSRP, + getSRPAttributes, + setupSRP, + verifySRP, } from "ente-accounts/services/srp"; -import type { SRPAttributes } from "ente-accounts/services/srp-remote"; -import { getSRPAttributes } from "ente-accounts/services/srp-remote"; +import { + generateAndSaveInteractiveKeyAttributes, + type KeyAttributes, + type User, +} from "ente-accounts/services/user"; +import { decryptAndStoreToken } from "ente-accounts/utils/helpers"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingIndicator } from "ente-base/components/loaders"; import { useBaseContext } from "ente-base/context"; -import { sharedCryptoWorker } from "ente-base/crypto"; -import type { B64EncryptionResult } from "ente-base/crypto/libsodium"; +import { decryptBox } from "ente-base/crypto"; import { clearLocalStorage } from "ente-base/local-storage"; import log from "ente-base/log"; -import VerifyMasterPasswordForm, { - type VerifyMasterPasswordFormProps, -} from "ente-shared/components/VerifyMasterPasswordForm"; import { - decryptAndStoreToken, - generateAndSaveIntermediateKeyAttributes, - generateLoginSubKey, - saveKeyInSessionStore, -} from "ente-shared/crypto/helpers"; + haveAuthenticatedSession, + saveMasterKeyInSessionAndSafeStore, + stashKeyEncryptionKeyInSessionStore, + unstashKeyEncryptionKeyFromSession, + updateSessionFromElectronSafeStorageIfNeeded, +} from "ente-base/session"; import { CustomError } from "ente-shared/error"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; import { @@ -47,8 +54,6 @@ import { isFirstLogin, setIsFirstLogin, } from "ente-shared/storage/localStorage/helpers"; -import { getKey, removeKey, setKey } from "ente-shared/storage/sessionStorage"; -import type { KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useCallback, useEffect, useState } from "react"; @@ -110,49 +115,31 @@ const Page: React.FC = () => { return; } setUser(user); - let key = getKey("encryptionKey"); - const electron = globalThis.electron; - if (!key && electron) { - try { - key = await electron.masterKeyB64(); - } catch (e) { - log.error("Failed to read master key from safe storage", e); - } - if (key) { - await saveKeyInSessionStore("encryptionKey", key, true); - } - } - const token = getToken(); - if (key && token) { + await updateSessionFromElectronSafeStorageIfNeeded(); + if (await haveAuthenticatedSession()) { void router.push(appHomeRoute); return; } - const kekEncryptedAttributes: B64EncryptionResult = - getKey("keyEncryptionKey"); + const kek = await unstashKeyEncryptionKeyFromSession(); const keyAttributes: KeyAttributes = getData("keyAttributes"); const srpAttributes: SRPAttributes = getData("srpAttributes"); - if (token) { + if (getToken()) { setSessionValidityCheck(validateSession()); } - if (kekEncryptedAttributes && keyAttributes) { - removeKey("keyEncryptionKey"); - const cryptoWorker = await sharedCryptoWorker(); - const kek = await cryptoWorker.decryptB64( - kekEncryptedAttributes.encryptedData, - kekEncryptedAttributes.nonce, - kekEncryptedAttributes.key, - ); - const key = await cryptoWorker.decryptB64( - keyAttributes.encryptedKey, - keyAttributes.keyDecryptionNonce, + if (kek && keyAttributes) { + const masterKey = await decryptBox( + { + encryptedData: keyAttributes.encryptedKey, + nonce: keyAttributes.keyDecryptionNonce, + }, kek, ); - // eslint-disable-next-line react-hooks/rules-of-hooks - useMasterPassword(key, kek, keyAttributes); + void postVerification(masterKey, kek, keyAttributes); return; } + if (keyAttributes) { if ( (!user?.token && !user?.encryptedToken) || @@ -182,13 +169,12 @@ const Page: React.FC = () => { async (kek: string) => { try { // Currently the page will get reloaded if any of the attributes - // have changed, so we don't need to worry about the kek having + // have changed, so we don't need to worry about the KEK having // been generated using stale credentials. This await on the // promise is here to only ensure we're done with the check // before we let the user in. if (sessionValidityCheck) await sessionValidityCheck; - const cryptoWorker = await sharedCryptoWorker(); const { keyAttributes, encryptedToken, @@ -199,14 +185,12 @@ const Page: React.FC = () => { accountsUrl, } = await userVerificationResultAfterResolvingSecondFactorChoice( - await loginViaSRP(srpAttributes!, kek), + await verifySRP(srpAttributes!, kek), ); setIsFirstLogin(true); if (passkeySessionID) { - const sessionKeyAttributes = - await cryptoWorker.generateKeyAndEncryptToB64(kek); - setKey("keyEncryptionKey", sessionKeyAttributes); + await stashKeyEncryptionKeyInSessionStore(kek); const user = getData("user"); await setLSUser({ ...user, @@ -223,9 +207,7 @@ const Page: React.FC = () => { openPasskeyVerificationURL({ passkeySessionID, url }); throw Error(CustomError.TWO_FACTOR_ENABLED); } else if (twoFactorSessionID) { - const sessionKeyAttributes = - await cryptoWorker.generateKeyAndEncryptToB64(kek); - setKey("keyEncryptionKey", sessionKeyAttributes); + await stashKeyEncryptionKeyInSessionStore(kek); const user = getData("user"); await setLSUser({ ...user, @@ -257,46 +239,44 @@ const Page: React.FC = () => { } }; - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const useMasterPassword: VerifyMasterPasswordFormProps["callback"] = async ( - key, - kek, - keyAttributes, - passphrase, + const handleVerifyMasterPassword: VerifyMasterPasswordFormProps["onVerify"] = + (key, kek, keyAttributes, password) => { + void (async () => { + const updatedKeyAttributes = isFirstLogin() + ? await generateAndSaveInteractiveKeyAttributes( + password, + keyAttributes, + key, + ) + : keyAttributes; + await postVerification(key, kek, updatedKeyAttributes); + })(); + }; + + const postVerification = async ( + masterKey: string, + kek: string, + keyAttributes: KeyAttributes, ) => { + await saveMasterKeyInSessionAndSafeStore(masterKey); + await decryptAndStoreToken(keyAttributes, masterKey); try { - if (isFirstLogin() && passphrase) { - await generateAndSaveIntermediateKeyAttributes( - passphrase, - keyAttributes, - key, - ); - } - await saveKeyInSessionStore("encryptionKey", key); - await decryptAndStoreToken(keyAttributes, key); - try { - let srpAttributes: SRPAttributes | null = - getData("srpAttributes"); - if (!srpAttributes && user) { - srpAttributes = await getSRPAttributes(user.email); - if (srpAttributes) { - setData("srpAttributes", srpAttributes); - } + let srpAttributes: SRPAttributes | null | undefined = + getData("srpAttributes"); + if (!srpAttributes && user) { + srpAttributes = await getSRPAttributes(user.email); + if (srpAttributes) { + setData("srpAttributes", srpAttributes); } - log.debug(() => `userSRPSetupPending ${!srpAttributes}`); - if (!srpAttributes) { - const loginSubKey = await generateLoginSubKey(kek); - const srpSetupAttributes = - await generateSRPSetupAttributes(loginSubKey); - await configureSRP(srpSetupAttributes); - } - } catch (e) { - log.error("migrate to srp failed", e); } - void router.push(unstashRedirect() ?? appHomeRoute); + log.debug(() => `userSRPSetupPending ${!srpAttributes}`); + if (!srpAttributes) { + await setupSRP(await generateSRPSetupAttributes(kek)); + } } catch (e) { - log.error("useMasterPassword failed", e); + log.error("migrate to srp failed", e); } + void router.push(unstashRedirect() ?? appHomeRoute); }; if (!keyAttributes && !srpAttributes) { @@ -333,17 +313,15 @@ const Page: React.FC = () => { // possibility using types. return ( - {user?.email ?? ""} - + - router.push("/recover")}> {t("forgot_password")} diff --git a/web/packages/accounts/pages/generate.tsx b/web/packages/accounts/pages/generate.tsx index 1c7211b9cd..e176bad56f 100644 --- a/web/packages/accounts/pages/generate.tsx +++ b/web/packages/accounts/pages/generate.tsx @@ -1,41 +1,46 @@ +import { Divider } from "@mui/material"; import { AccountsPageContents, AccountsPageFooter, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; import { RecoveryKey } from "ente-accounts/components/RecoveryKey"; -import SetPasswordForm, { - type SetPasswordFormProps, -} from "ente-accounts/components/SetPasswordForm"; import { appHomeRoute } from "ente-accounts/services/redirect"; import { - configureSRP, - generateKeyAndSRPAttributes, + generateSRPSetupAttributes, + setupSRP, } from "ente-accounts/services/srp"; -import { putAttributes } from "ente-accounts/services/user"; +import type { KeyAttributes, User } from "ente-accounts/services/user"; +import { + generateAndSaveInteractiveKeyAttributes, + generateKeysAndAttributes, + putUserKeyAttributes, +} from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingIndicator } from "ente-base/components/loaders"; import { useBaseContext } from "ente-base/context"; +import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types"; import log from "ente-base/log"; import { - generateAndSaveIntermediateKeyAttributes, - saveKeyInSessionStore, -} from "ente-shared/crypto/helpers"; + haveCredentialsInSession, + saveMasterKeyInSessionAndSafeStore, +} from "ente-base/session"; import { getData } from "ente-shared/storage/localStorage"; import { justSignedUp, setJustSignedUp, } from "ente-shared/storage/localStorage/helpers"; -import { getKey } from "ente-shared/storage/sessionStorage"; -import type { KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; +import { + NewPasswordForm, + type NewPasswordFormProps, +} from "../components/NewPasswordForm"; const Page: React.FC = () => { const { logout, showMiniDialog } = useBaseContext(); - const [token, setToken] = useState(); const [user, setUser] = useState(); const [openRecoveryKey, setOpenRecoveryKey] = useState(false); const [loading, setLoading] = useState(true); @@ -43,13 +48,12 @@ const Page: React.FC = () => { const router = useRouter(); useEffect(() => { - const key: string = getKey("encryptionKey"); const keyAttributes: KeyAttributes = getData("originalKeyAttributes"); const user: User = getData("user"); setUser(user); if (!user?.token) { void router.push("/"); - } else if (key) { + } else if (haveCredentialsInSession()) { if (justSignedUp()) { setOpenRecoveryKey(true); setLoading(false); @@ -59,33 +63,35 @@ const Page: React.FC = () => { } else if (keyAttributes?.encryptedKey) { void router.push("/credentials"); } else { - setToken(user.token); setLoading(false); } }, [router]); - const onSubmit: SetPasswordFormProps["callback"] = async ( - passphrase, - setFieldError, + const handleSubmit: NewPasswordFormProps["onSubmit"] = async ( + password, + setPasswordsFieldError, ) => { try { - const { keyAttributes, masterKey, srpSetupAttributes } = - await generateKeyAndSRPAttributes(passphrase); - - // TODO: Refactor the code to not require this ensure - await putAttributes(token!, keyAttributes); - await configureSRP(srpSetupAttributes); - await generateAndSaveIntermediateKeyAttributes( - passphrase, + const { masterKey, kek, keyAttributes } = + await generateKeysAndAttributes(password); + await putUserKeyAttributes(keyAttributes); + await setupSRP(await generateSRPSetupAttributes(kek)); + await generateAndSaveInteractiveKeyAttributes( + password, keyAttributes, masterKey, ); - await saveKeyInSessionStore("encryptionKey", masterKey); + await saveMasterKeyInSessionAndSafeStore(masterKey); setJustSignedUp(true); setOpenRecoveryKey(true); } catch (e) { log.error("failed to generate password", e); - setFieldError("passphrase", t("password_generation_failed")); + setPasswordsFieldError( + e instanceof Error && + e.message == deriveKeyInsufficientMemoryErrorMessage + ? t("password_generation_failed") + : t("generic_error"), + ); } }; @@ -102,11 +108,12 @@ const Page: React.FC = () => { ) : ( {t("set_password")} - + {t("go_back")} diff --git a/web/packages/accounts/pages/passkeys/finish.tsx b/web/packages/accounts/pages/passkeys/finish.tsx index 44e4825f2b..c55a7b6117 100644 --- a/web/packages/accounts/pages/passkeys/finish.tsx +++ b/web/packages/accounts/pages/passkeys/finish.tsx @@ -1,6 +1,6 @@ import { unstashRedirect } from "ente-accounts/services/redirect"; import { LoadingIndicator } from "ente-base/components/loaders"; -import { fromB64URLSafeNoPaddingString } from "ente-base/crypto/libsodium"; +import { fromB64URLSafeNoPadding } from "ente-base/crypto"; import log from "ente-base/log"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; import { nullToUndefined } from "ente-utils/transform"; @@ -72,7 +72,7 @@ const saveCredentialsAndNavigateTo = async ( // Decode response string (inverse of the steps we perform in // `passkeyAuthenticationSuccessRedirectURL`). const decodedResponse = JSON.parse( - await fromB64URLSafeNoPaddingString(response), + new TextDecoder().decode(await fromB64URLSafeNoPadding(response)), ); // Only one of `encryptedToken` or `token` will be present depending on the @@ -81,7 +81,8 @@ const saveCredentialsAndNavigateTo = async ( // - The plaintext "token" will be passed during fresh signups, where we // don't yet have keys to encrypt it, the account itself is being created // as we go through this flow. - // TODO(MR): Conceptually this cannot happen. During a _real_ fresh signup + // + // TODO: Conceptually this cannot happen. During a _real_ fresh signup // we'll never enter the passkey verification flow. Remove this code after // making sure that it doesn't get triggered in cases where an existing // user goes through the new user flow. diff --git a/web/packages/accounts/pages/recover.tsx b/web/packages/accounts/pages/recover.tsx index 179edbc155..2d36583b7f 100644 --- a/web/packages/accounts/pages/recover.tsx +++ b/web/packages/accounts/pages/recover.tsx @@ -3,31 +3,28 @@ import { AccountsPageFooter, AccountsPageTitle, } from "ente-accounts/components/layouts/centered-paper"; +import { recoveryKeyFromMnemonic } from "ente-accounts/services/recovery-key"; import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect"; +import type { KeyAttributes, User } from "ente-accounts/services/user"; import { sendOTT } from "ente-accounts/services/user"; +import { decryptAndStoreToken } from "ente-accounts/utils/helpers"; import { LinkButton } from "ente-base/components/LinkButton"; -import { useBaseContext } from "ente-base/context"; -import { sharedCryptoWorker } from "ente-base/crypto"; -import log from "ente-base/log"; -import SingleInputForm, { - type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; import { - decryptAndStoreToken, - saveKeyInSessionStore, -} from "ente-shared/crypto/helpers"; + SingleInputForm, + type SingleInputFormProps, +} from "ente-base/components/SingleInputForm"; +import { useBaseContext } from "ente-base/context"; +import { decryptBox } from "ente-base/crypto"; +import log from "ente-base/log"; +import { + haveCredentialsInSession, + saveMasterKeyInSessionAndSafeStore, +} from "ente-base/session"; import { getData, setData } from "ente-shared/storage/localStorage"; -import { getKey } from "ente-shared/storage/sessionStorage"; -import type { KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const bip39 = require("bip39"); -// mobile client library only supports english. -bip39.setDefaultWordlist("english"); - const Page: React.FC = () => { const { showMiniDialog } = useBaseContext(); @@ -40,7 +37,6 @@ const Page: React.FC = () => { useEffect(() => { const user: User = getData("user"); const keyAttributes: KeyAttributes = getData("keyAttributes"); - const key = getKey("encryptionKey"); if (!user?.email) { void router.push("/"); return; @@ -53,39 +49,27 @@ const Page: React.FC = () => { } if (!keyAttributes) { void router.push("/generate"); - } else if (key) { + } else if (haveCredentialsInSession()) { void router.push(appHomeRoute); } else { setKeyAttributes(keyAttributes); } }, [router]); - const recover: SingleInputFormProps["callback"] = async ( - recoveryKey: string, + const handleSubmit: SingleInputFormProps["onSubmit"] = async ( + recoveryKeyMnemonic: string, setFieldError, ) => { try { - recoveryKey = recoveryKey - .trim() - .split(" ") - .map((part) => part.trim()) - .filter((part) => !!part) - .join(" "); - // check if user is entering mnemonic recovery key - if (recoveryKey.indexOf(" ") > 0) { - if (recoveryKey.split(" ").length !== 24) { - throw new Error("recovery code should have 24 words"); - } - recoveryKey = bip39.mnemonicToEntropy(recoveryKey); - } - const cryptoWorker = await sharedCryptoWorker(); const keyAttr = keyAttributes!; - const masterKey = await cryptoWorker.decryptB64( - keyAttr.masterKeyEncryptedWithRecoveryKey!, - keyAttr.masterKeyDecryptionNonce!, - await cryptoWorker.fromHex(recoveryKey), + const masterKey = await decryptBox( + { + encryptedData: keyAttr.masterKeyEncryptedWithRecoveryKey!, + nonce: keyAttr.masterKeyDecryptionNonce!, + }, + await recoveryKeyFromMnemonic(recoveryKeyMnemonic), ); - await saveKeyInSessionStore("encryptionKey", masterKey); + await saveMasterKeyInSessionAndSafeStore(masterKey); await decryptAndStoreToken(keyAttr, masterKey); setData("showBackButton", { value: false }); @@ -108,11 +92,10 @@ const Page: React.FC = () => { {t("recover_account")} diff --git a/web/packages/accounts/pages/two-factor/recover.tsx b/web/packages/accounts/pages/two-factor/recover.tsx index f0aba30440..3cbfbd776f 100644 --- a/web/packages/accounts/pages/two-factor/recover.tsx +++ b/web/packages/accounts/pages/two-factor/recover.tsx @@ -1,5 +1,4 @@ import { Link } from "@mui/material"; -import { HttpStatusCode } from "axios"; import { AccountsPageContents, AccountsPageFooter, @@ -7,49 +6,67 @@ import { } from "ente-accounts/components/layouts/centered-paper"; import { recoverTwoFactor, - removeTwoFactor, + recoverTwoFactorFinish, + type TwoFactorRecoveryResponse, type TwoFactorType, } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; +import { LoadingIndicator } from "ente-base/components/loaders"; import type { MiniDialogAttributes } from "ente-base/components/MiniDialog"; -import { useBaseContext } from "ente-base/context"; -import { sharedCryptoWorker } from "ente-base/crypto"; -import type { B64EncryptionResult } from "ente-base/crypto/libsodium"; -import log from "ente-base/log"; -import SingleInputForm, { +import { + SingleInputForm, type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; -import { ApiError } from "ente-shared/error"; -import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; +} from "ente-base/components/SingleInputForm"; +import { useBaseContext } from "ente-base/context"; +import { isHTTP4xxError, isHTTPErrorWithStatus } from "ente-base/http"; +import log from "ente-base/log"; +import { getData } from "ente-shared/storage/localStorage"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Trans } from "react-i18next"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const bip39 = require("bip39"); -// mobile client library only supports english. -bip39.setDefaultWordlist("english"); - export interface RecoverPageProps { twoFactorType: TwoFactorType; } +/** + * A page where the user can enter their recovery key to reset or bypass their + * second factor in case they no longer have access to it. + */ const Page: React.FC = ({ twoFactorType }) => { - const { logout, showMiniDialog } = useBaseContext(); + const { logout, showMiniDialog, onGenericError } = useBaseContext(); - const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = - useState | null>(null); - const [sessionID, setSessionID] = useState(null); - const [doesHaveEncryptedRecoveryKey, setDoesHaveEncryptedRecoveryKey] = - useState(false); + const [sessionID, setSessionID] = useState(undefined); + const [recoveryResponse, setRecoveryResponse] = useState< + TwoFactorRecoveryResponse | undefined + >(undefined); const router = useRouter(); + const showContactSupportDialog = useCallback( + (dialogContinue?: MiniDialogAttributes["continue"]) => + showMiniDialog({ + title: t("contact_support"), + message: ( + , + }} + values={{ emailID: "support@ente.io" }} + /> + ), + continue: { color: "secondary", ...(dialogContinue ?? {}) }, + cancel: false, + }), + [showMiniDialog], + ); + useEffect(() => { const user = getData("user"); - const sid = user.passkeySessionID || user.twoFactorSessionID; - if (!user?.email || !sid) { + const sessionID = user.passkeySessionID || user.twoFactorSessionID; + if (!user?.email || !sessionID) { void router.push("/"); } else if ( !(user.isTwoFactorEnabled || user.isTwoFactorEnabledPasskey) && @@ -57,115 +74,59 @@ const Page: React.FC = ({ twoFactorType }) => { ) { void router.push("/generate"); } else { - setSessionID(sid); + setSessionID(sessionID); + void recoverTwoFactor(twoFactorType, sessionID) + .then(setRecoveryResponse) + .catch((e: unknown) => { + log.error("Second factor recovery page setup failed", e); + if (isHTTPErrorWithStatus(e, 404)) { + logout(); + } else if (isHTTP4xxError(e)) { + showContactSupportDialog({ action: router.back }); + } else { + onGenericError(e); + } + }); } - const main = async () => { - try { - const resp = await recoverTwoFactor(sid, twoFactorType); - setDoesHaveEncryptedRecoveryKey(!!resp.encryptedSecret); - if (!resp.encryptedSecret) { - showContactSupportDialog({ action: router.back }); - } else { - setEncryptedTwoFactorSecret({ - encryptedData: resp.encryptedSecret, - nonce: resp.secretDecryptionNonce, - }); - } - } catch (e) { - if ( - e instanceof ApiError && - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - e.httpStatusCode === HttpStatusCode.NotFound - ) { - logout(); - } else { - log.error("two factor recovery page setup failed", e); - setDoesHaveEncryptedRecoveryKey(false); - showContactSupportDialog({ action: router.back }); - } - } - }; - void main(); - // TODO: - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [ + twoFactorType, + logout, + showContactSupportDialog, + onGenericError, + router, + ]); - const recover: SingleInputFormProps["callback"] = async ( - recoveryKey: string, - setFieldError, - ) => { - try { - recoveryKey = recoveryKey - .trim() - .split(" ") - .map((part) => part.trim()) - .filter((part) => !!part) - .join(" "); - // check if user is entering mnemonic recovery key - if (recoveryKey.indexOf(" ") > 0) { - if (recoveryKey.split(" ").length !== 24) { - throw new Error("recovery code should have 24 words"); - } - recoveryKey = bip39.mnemonicToEntropy(recoveryKey); - } - const cryptoWorker = await sharedCryptoWorker(); - const { encryptedData, nonce } = encryptedTwoFactorSecret!; - const twoFactorSecret = await cryptoWorker.decryptB64( - encryptedData, - nonce, - await cryptoWorker.fromHex(recoveryKey), - ); - const resp = await removeTwoFactor( - sessionID!, - twoFactorSecret, - twoFactorType, - ); - const { keyAttributes, encryptedToken, token, id } = resp; - await setLSUser({ - ...getData("user"), - token, - encryptedToken, - id, - isTwoFactorEnabled: false, - }); - setData("keyAttributes", keyAttributes); - void router.push("/credentials"); - } catch (e) { - log.error("two factor recovery failed", e); - setFieldError(t("incorrect_recovery_key")); - } - }; + const handleSubmit: SingleInputFormProps["onSubmit"] | undefined = useMemo( + () => + sessionID && recoveryResponse + ? (recoveryKeyMnemonic, setFieldError) => + recoverTwoFactorFinish( + twoFactorType, + sessionID, + recoveryResponse, + recoveryKeyMnemonic, + ) + .then(() => router.push("/credentials")) + .catch((e: unknown) => { + log.error("Second factor recovery failed", e); + setFieldError(t("incorrect_recovery_key")); + }) + : undefined, + [twoFactorType, router, sessionID, recoveryResponse], + ); - const showContactSupportDialog = ( - dialogContinue?: MiniDialogAttributes["continue"], - ) => { - showMiniDialog({ - title: t("contact_support"), - message: ( - }} - values={{ emailID: "support@ente.io" }} - /> - ), - continue: { color: "secondary", ...(dialogContinue ?? {}) }, - cancel: false, - }); - }; - - if (!doesHaveEncryptedRecoveryKey) { - return <>; + if (!handleSubmit) { + return ; } return ( {t("recover_two_factor")} showContactSupportDialog()}> diff --git a/web/packages/accounts/pages/two-factor/setup.tsx b/web/packages/accounts/pages/two-factor/setup.tsx index ec61ccacfe..135af20f68 100644 --- a/web/packages/accounts/pages/two-factor/setup.tsx +++ b/web/packages/accounts/pages/two-factor/setup.tsx @@ -3,13 +3,14 @@ import { CodeBlock } from "ente-accounts/components/CodeBlock"; import { Verify2FACodeForm } from "ente-accounts/components/Verify2FACodeForm"; import { appHomeRoute } from "ente-accounts/services/redirect"; import type { TwoFactorSecret } from "ente-accounts/services/user"; -import { enableTwoFactor, setupTwoFactor } from "ente-accounts/services/user"; +import { + setupTwoFactor, + setupTwoFactorFinish, +} from "ente-accounts/services/user"; import { CenteredFill } from "ente-base/components/containers"; import { LinkButton } from "ente-base/components/LinkButton"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; -import { encryptWithRecoveryKey } from "ente-shared/crypto/helpers"; -import { getData, setLSUser } from "ente-shared/storage/localStorage"; import { t } from "i18next"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; @@ -26,16 +27,7 @@ const Page: React.FC = () => { }, []); const handleSubmit = async (otp: string) => { - const { - encryptedData: encryptedTwoFactorSecret, - nonce: twoFactorSecretDecryptionNonce, - } = await encryptWithRecoveryKey(twoFactorSecret!.secretCode); - await enableTwoFactor({ - code: otp, - encryptedTwoFactorSecret, - twoFactorSecretDecryptionNonce, - }); - await setLSUser({ ...getData("user"), isTwoFactorEnabled: true }); + await setupTwoFactorFinish(twoFactorSecret!.secretCode, otp); await router.push(appHomeRoute); }; diff --git a/web/packages/accounts/pages/two-factor/verify.tsx b/web/packages/accounts/pages/two-factor/verify.tsx index 287f987c57..b961959790 100644 --- a/web/packages/accounts/pages/two-factor/verify.tsx +++ b/web/packages/accounts/pages/two-factor/verify.tsx @@ -1,10 +1,10 @@ import { Verify2FACodeForm } from "ente-accounts/components/Verify2FACodeForm"; +import type { User } from "ente-accounts/services/user"; import { verifyTwoFactor } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { useBaseContext } from "ente-base/context"; -import { HTTPError } from "ente-base/http"; +import { isHTTPErrorWithStatus } from "ente-base/http"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; -import type { User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -38,13 +38,23 @@ const Page: React.FC = () => { const handleSubmit = async (otp: string) => { try { - const resp = await verifyTwoFactor(otp, sessionID); - const { keyAttributes, encryptedToken, token, id } = resp; - await setLSUser({ ...getData("user"), token, encryptedToken, id }); - setData("keyAttributes", keyAttributes!); + const { keyAttributes, encryptedToken, id } = await verifyTwoFactor( + otp, + sessionID, + ); + await setLSUser({ + ...getData("user"), + id, + // The original code was parsing an token which is never going + // to be present in the response, so effectively was always + // setting token to undefined. So this works, but is it needed? + token: undefined, + encryptedToken, + }); + setData("keyAttributes", keyAttributes); await router.push(unstashRedirect() ?? "/credentials"); } catch (e) { - if (e instanceof HTTPError && e.res.status == 404) { + if (isHTTPErrorWithStatus(e, 404)) { logout(); } else { throw e; diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index 395278ab16..c8f8b26ad0 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -16,24 +16,29 @@ import { stashedRedirect, unstashRedirect, } from "ente-accounts/services/redirect"; -import { configureSRP } from "ente-accounts/services/srp"; -import type { - SRPAttributes, - SRPSetupAttributes, -} from "ente-accounts/services/srp-remote"; -import { getSRPAttributes } from "ente-accounts/services/srp-remote"; import { - putAttributes, + getSRPAttributes, + setupSRP, + unstashAndUseSRPSetupAttributes, + type SRPAttributes, +} from "ente-accounts/services/srp"; +import type { KeyAttributes, User } from "ente-accounts/services/user"; +import { + putUserKeyAttributes, sendOTT, verifyEmail, } from "ente-accounts/services/user"; import { LinkButton } from "ente-base/components/LinkButton"; import { LoadingIndicator } from "ente-base/components/loaders"; -import { useBaseContext } from "ente-base/context"; -import log from "ente-base/log"; -import SingleInputForm, { +import { + SingleInputForm, type SingleInputFormProps, -} from "ente-shared/components/SingleInputForm"; +} from "ente-base/components/SingleInputForm"; +import { useBaseContext } from "ente-base/context"; +import { isDevBuild } from "ente-base/env"; +import { isHTTPErrorWithStatus } from "ente-base/http"; +import log from "ente-base/log"; +import { clearSessionStorage } from "ente-base/session"; import { ApiError } from "ente-shared/error"; import localForage from "ente-shared/storage/localForage"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; @@ -41,8 +46,6 @@ import { getLocalReferralSource, setIsFirstLogin, } from "ente-shared/storage/localStorage/helpers"; -import { clearKeys } from "ente-shared/storage/sessionStorage"; -import type { KeyAttributes, User } from "ente-shared/user/types"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -77,11 +80,12 @@ const Page: React.FC = () => { void main(); }, [router]); - const onSubmit: SingleInputFormProps["callback"] = async ( + const onSubmit: SingleInputFormProps["onSubmit"] = async ( ott, setFieldError, ) => { try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call const referralSource = getLocalReferralSource()?.trim(); const cleanedReferral = referralSource ? `web:${referralSource}` @@ -137,30 +141,35 @@ const Page: React.FC = () => { setData("keyAttributes", keyAttributes); setData("originalKeyAttributes", keyAttributes); } else { - if (getData("originalKeyAttributes")) { - await putAttributes( - token!, - getData("originalKeyAttributes"), - ); - } - if (getData("srpSetupAttributes")) { - const srpSetupAttributes: SRPSetupAttributes = - getData("srpSetupAttributes"); - await configureSRP(srpSetupAttributes); + const originalKeyAttributes = getData( + "originalKeyAttributes", + ); + if (originalKeyAttributes) { + await putUserKeyAttributes(originalKeyAttributes); } + await unstashAndUseSRPSetupAttributes(setupSRP); + } + // TODO(RE): Temporary safety valve before removing the + // unnecessary clear (tag: Migration) + if (isDevBuild && (await localForage.length()) > 0) { + throw new Error("Local forage is not empty"); } await localForage.clear(); setIsFirstLogin(true); const redirectURL = unstashRedirect(); if (keyAttributes?.encryptedKey) { - clearKeys(); + clearSessionStorage(); void router.push(redirectURL ?? "/credentials"); } else { void router.push(redirectURL ?? "/generate"); } } } catch (e) { - if (e instanceof ApiError) { + if (isHTTPErrorWithStatus(e, 401)) { + setFieldError(t("invalid_code_error")); + } else if (isHTTPErrorWithStatus(e, 410)) { + setFieldError(t("expired_code_error")); + } else if (e instanceof ApiError) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (e?.httpStatusCode === HttpStatusCode.Unauthorized) { setFieldError(t("invalid_code_error")); @@ -170,7 +179,7 @@ const Page: React.FC = () => { } } else { log.error("OTT verification failed", e); - setFieldError(t("generic_error_retry")); + throw e; } } }; @@ -236,11 +245,10 @@ const Page: React.FC = () => { {t("check_inbox_hint")} diff --git a/web/packages/accounts/services/logout.ts b/web/packages/accounts/services/logout.ts index 8d0446189c..5ea2c41fa3 100644 --- a/web/packages/accounts/services/logout.ts +++ b/web/packages/accounts/services/logout.ts @@ -2,8 +2,8 @@ import { clearBlobCaches } from "ente-base/blob-cache"; import { clearKVDB } from "ente-base/kv"; import { clearLocalStorage } from "ente-base/local-storage"; import log from "ente-base/log"; +import { clearSessionStorage } from "ente-base/session"; import localForage from "ente-shared/storage/localForage"; -import { clearKeys } from "ente-shared/storage/sessionStorage"; import { clearStashedRedirect } from "./redirect"; import { remoteLogoutIfNeeded } from "./user"; @@ -34,7 +34,7 @@ export const accountLogout = async () => { ignoreError("In-memory store", e); } try { - clearKeys(); + clearSessionStorage(); } catch (e) { ignoreError("Session storage", e); } diff --git a/web/packages/accounts/services/passkey.ts b/web/packages/accounts/services/passkey.ts index 3e7cd99683..70410ff639 100644 --- a/web/packages/accounts/services/passkey.ts +++ b/web/packages/accounts/services/passkey.ts @@ -1,23 +1,16 @@ import { TwoFactorAuthorizationResponse } from "ente-accounts/services/user"; import { clientPackageName, isDesktop } from "ente-base/app"; -import { sharedCryptoWorker } from "ente-base/crypto"; -import { - encryptToB64, - generateEncryptionKey, -} from "ente-base/crypto/libsodium"; +import { encryptBox, generateKey } from "ente-base/crypto"; import { authenticatedRequestHeaders, ensureOk, HTTPError, publicRequestHeaders, } from "ente-base/http"; -import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; -import { getRecoveryKey } from "ente-shared/crypto/helpers"; -import HTTPService from "ente-shared/network/HTTPService"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; -import { z } from "zod"; +import { z } from "zod/v4"; +import { getUserRecoveryKey } from "./recovery-key"; import { unstashRedirect } from "./redirect"; /** @@ -106,26 +99,17 @@ export const openPasskeyVerificationURL = ({ * see and their manage their passkeys. */ export const openAccountsManagePasskeysPage = async () => { - // Check if the user has passkey recovery enabled - const recoveryEnabled = await isPasskeyRecoveryEnabled(); - if (!recoveryEnabled) { + // Check if the user has passkey recovery enabled. + const { isPasskeyRecoveryEnabled } = await getTwoFactorRecoveryStatus(); + if (!isPasskeyRecoveryEnabled) { // If not, enable it for them by creating the necessary recovery // information to prevent them from getting locked out. - const recoveryKey = await getRecoveryKey(); - - const resetSecret = await generateEncryptionKey(); - - const cryptoWorker = await sharedCryptoWorker(); - const encryptionResult = await encryptToB64( + const resetSecret = await generateKey(); + const { encryptedData, nonce } = await encryptBox( resetSecret, - await cryptoWorker.fromHex(recoveryKey), - ); - - await configurePasskeyRecovery( - resetSecret, - encryptionResult.encryptedData, - encryptionResult.nonce, + await getUserRecoveryKey(), ); + await configurePasskeyRecovery(resetSecret, encryptedData, nonce); } // Redirect to the Ente Accounts app where they can view and add and manage @@ -137,50 +121,47 @@ export const openAccountsManagePasskeysPage = async () => { window.open(`${accountsURL}/passkeys?${params.toString()}`); }; -export const isPasskeyRecoveryEnabled = async () => { - try { - const token = getToken(); +const TwoFactorRecoveryStatus = z.object({ + /** + * `true` if the passkey recovery setup has been completed. + */ + isPasskeyRecoveryEnabled: z.boolean(), +}); - const resp = await HTTPService.get( - await apiURL("/users/two-factor/recovery-status"), - {}, - { "X-Auth-Token": token }, - ); - - if (typeof resp.data == "undefined") { - throw Error("request failed"); - } - - return resp.data.isPasskeyRecoveryEnabled as boolean; - } catch (e) { - log.error("failed to get passkey recovery status", e); - throw e; - } +/** + * Obtain the second factor recovery status from remote. + */ +export const getTwoFactorRecoveryStatus = async () => { + const res = await fetch(await apiURL("/users/two-factor/recovery-status"), { + headers: await authenticatedRequestHeaders(), + }); + ensureOk(res); + return TwoFactorRecoveryStatus.parse(await res.json()); }; +/** + * Allow the user to bypass their passkeys by saving the provided recovery + * credentials on remote. + */ const configurePasskeyRecovery = async ( secret: string, userSecretCipher: string, userSecretNonce: string, -) => { - try { - const token = getToken(); - - const resp = await HTTPService.post( +) => + ensureOk( + await fetch( await apiURL("/users/two-factor/passkeys/configure-recovery"), - { secret, userSecretCipher, userSecretNonce }, - undefined, - { "X-Auth-Token": token }, - ); - - if (typeof resp.data == "undefined") { - throw Error("request failed"); - } - } catch (e) { - log.error("failed to configure passkey recovery", e); - throw e; - } -}; + { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify({ + secret, + userSecretCipher, + userSecretNonce, + }), + }, + ), + ); /** * Fetch an Ente Accounts specific JWT token. @@ -264,7 +245,7 @@ export const saveCredentialsAndNavigateTo = async ( const { id, encryptedToken, keyAttributes } = response; await setLSUser({ ...getData("user"), encryptedToken, id }); - setData("keyAttributes", keyAttributes!); + setData("keyAttributes", keyAttributes); return unstashRedirect() ?? "/credentials"; }; diff --git a/web/packages/accounts/services/recovery-key.ts b/web/packages/accounts/services/recovery-key.ts new file mode 100644 index 0000000000..1d8b29009c --- /dev/null +++ b/web/packages/accounts/services/recovery-key.ts @@ -0,0 +1,121 @@ +import * as bip39 from "bip39"; +import type { KeyAttributes } from "ente-accounts/services/user"; +import { + decryptBox, + fromHex, + sharedCryptoWorker, + toHex, +} from "ente-base/crypto"; +import { ensureMasterKeyFromSession } from "ente-base/session"; +import { getData } from "ente-shared/storage/localStorage"; +import { putUserRecoveryKeyAttributes, saveKeyAttributes } from "./user"; + +// Mobile client library only supports English. +bip39.setDefaultWordlist("english"); + +/** + * Convert the provided BIP-39 mnemonic string into its base64 representation. + * + * @param recoveryKeyMnemonicOrHex The BIP-39 mnemonic (24 word) string + * representing the recovery key. For legacy compatibility, the function also + * works if provided the hex representation of the recovery key. + * + * @returns A base64 string representing the underlying bytes of the recovery key. + */ +export const recoveryKeyFromMnemonic = (recoveryKeyMnemonicOrHex: string) => { + const trimmedInput = recoveryKeyMnemonicOrHex + .trim() + .split(" ") + .map((part) => part.trim()) + .filter((part) => !!part) + .join(" "); + + let recoveryKeyHex: string; + // Check if user is entering mnemonic recovery key. + if (trimmedInput.indexOf(" ") > 0) { + if (trimmedInput.split(" ").length != 24) { + throw new Error("recovery code should have 24 words"); + } + recoveryKeyHex = bip39.mnemonicToEntropy(trimmedInput); + } else { + recoveryKeyHex = trimmedInput; + } + + return fromHex(recoveryKeyHex); +}; + +/** + * Convert the provided base64 encoded recovery key into its BIP-39 mnemonic. + * + * @param recoveryKey The base64 encoded recovery key to mnemonize. + * + * @returns A 24-word mnemonic that serves as the user visible recovery key. + */ +export const recoveryKeyToMnemonic = async (recoveryKey: string) => + bip39.entropyToMnemonic(await toHex(recoveryKey)); + +/** + * Return the (decrypted) recovery key of the logged in user, reading it from + * local storage. + * + * As a fallback for old accounts that generated recovery keys on first view, + * this function will also generate a new recovery key if needed. + * + * @returns The user's base64 encoded recovery key. + */ +export const getUserRecoveryKey = async () => { + const masterKey = await ensureMasterKeyFromSession(); + + const keyAttributes: KeyAttributes = getData("keyAttributes"); + const { recoveryKeyEncryptedWithMasterKey, recoveryKeyDecryptionNonce } = + keyAttributes; + + if (recoveryKeyEncryptedWithMasterKey && recoveryKeyDecryptionNonce) { + return decryptBox( + { + encryptedData: recoveryKeyEncryptedWithMasterKey, + nonce: recoveryKeyDecryptionNonce, + }, + masterKey, + ); + } else { + return createNewRecoveryKey(masterKey); + } +}; + +/** + * Generate a new recovery key, tell remote about it, update our local state, + * and then return it. + * + * This function is meant only for (very!) old accounts for whom the app did not + * generate recovery keys on sign up but instead generated them on first view. + * + * @returns a new base64 encoded recovery key. + */ +const createNewRecoveryKey = async (masterKey: string) => { + const existingAttributes = getData("keyAttributes"); + + const cryptoWorker = await sharedCryptoWorker(); + const recoveryKey = await cryptoWorker.generateKey(); + const encryptedMasterKey = await cryptoWorker.encryptBox( + masterKey, + recoveryKey, + ); + const encryptedRecoveryKey = await cryptoWorker.encryptBox( + recoveryKey, + masterKey, + ); + + const recoveryKeyAttributes = { + masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData, + masterKeyDecryptionNonce: encryptedMasterKey.nonce, + recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData, + recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce, + }; + + await putUserRecoveryKeyAttributes(recoveryKeyAttributes); + + saveKeyAttributes({ ...existingAttributes, ...recoveryKeyAttributes }); + + return recoveryKey; +}; diff --git a/web/packages/accounts/services/session.ts b/web/packages/accounts/services/session.ts index a64b181153..05e4c64d97 100644 --- a/web/packages/accounts/services/session.ts +++ b/web/packages/accounts/services/session.ts @@ -1,10 +1,17 @@ +import type { KeyAttributes } from "ente-accounts/services/user"; import { authenticatedRequestHeaders, HTTPError } from "ente-base/http"; -import { ensureLocalUser } from "ente-base/local-user"; +import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; +import { getAuthToken } from "ente-base/token"; import { getData } from "ente-shared/storage/localStorage"; -import type { KeyAttributes } from "ente-shared/user/types"; -import type { SRPAttributes } from "./srp-remote"; -import { getSRPAttributes } from "./srp-remote"; +import { nullToUndefined } from "ente-utils/transform"; +import { z } from "zod/v4"; +import { getSRPAttributes, type SRPAttributes } from "./srp"; +import { + ensureLocalUser, + putUserKeyAttributes, + RemoteKeyAttributes, +} from "./user"; type SessionValidity = | { status: "invalid" } @@ -15,6 +22,11 @@ type SessionValidity = updatedSRPAttributes: SRPAttributes; }; +const SessionValidityResponse = z.object({ + hasSetKeys: z.boolean(), + keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined), +}); + /** * Check if the local token and/or key attributes we have are still valid. * @@ -72,16 +84,9 @@ export const checkSessionValidity = async (): Promise => { // See if the response contains keyAttributes (they might not for older // deployments). - const json = await res.json(); - if ( - "keyAttributes" in json && - typeof json.keyAttributes == "object" && - json.keyAttributes !== null - ) { - // Assume it is a `KeyAttributes`. - // - // Enhancement: Convert this to a zod validation. - const remoteKeyAttributes = json.keyAttributes as KeyAttributes; + const { keyAttributes } = SessionValidityResponse.parse(await res.json()); + if (keyAttributes) { + const remoteKeyAttributes = keyAttributes; // We should have these values locally if we reach here. const email = ensureLocalUser().email; @@ -106,7 +111,10 @@ export const checkSessionValidity = async (): Promise => { // changed. return { status: "validButPasswordChanged", - updatedKeyAttributes: remoteKeyAttributes, + // TODO: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + updatedKeyAttributes: remoteKeyAttributes as KeyAttributes, updatedSRPAttributes: remoteSRPAttributes, }; } @@ -116,3 +124,51 @@ export const checkSessionValidity = async (): Promise => { // The token is still valid (to the best of our ascertainable knowledge). return { status: "valid" }; }; + +/** + * Return `true` if the user does not have a saved auth token, of it is no + * longer valid. If needed, also update the key attributes at remote. + * + * This is a subset of {@link checkSessionValidity} that has been tailored for + * use during each remote sync, to detect if the user has been logged out + * elsewhere. + * + * @returns `true` if either we don't have an auth token, or if remote tells us + * that the auth token (and the associated session) has been invalidated. In all + * other cases, return `false`. + * + * In particular, this function doesn't throw and instead returns `false` on + * errors. This is because returning `true` will trigger a blocking alert that + * ends in logging the user out, and we don't want to log the user out on on + * e.g. transient network issues. + */ +export const isSessionInvalid = async (): Promise => { + const token = await getAuthToken(); + if (!token) { + return true; /* No saved token, session is invalid */ + } + + try { + const res = await fetch(await apiURL("/users/session-validity/v2"), { + headers: await authenticatedRequestHeaders(), + }); + if (!res.ok) { + if (res.status == 401) return true; /* session is no longer valid */ + else throw new HTTPError(res); + } + + const { hasSetKeys } = SessionValidityResponse.parse(await res.json()); + if (!hasSetKeys) { + const originalKeyAttributes = getData("originalKeyAttributes"); + if (originalKeyAttributes) + await putUserKeyAttributes(originalKeyAttributes); + } + } catch (e) { + log.warn("Failed to check session validity", e); + // Don't logout user on potentially transient errors. + return false; + } + + // Everything seems ok. + return false; +}; diff --git a/web/packages/accounts/services/srp-remote.ts b/web/packages/accounts/services/srp-remote.ts deleted file mode 100644 index 85823f8b3f..0000000000 --- a/web/packages/accounts/services/srp-remote.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { HttpStatusCode } from "axios"; -import { ensureOk, publicRequestHeaders } from "ente-base/http"; -import log from "ente-base/log"; -import { apiURL } from "ente-base/origins"; -import { ApiError, CustomError } from "ente-shared/error"; -import HTTPService from "ente-shared/network/HTTPService"; -import type { UpdatedKey, UserVerificationResponse } from "./user"; - -export interface SRPAttributes { - srpUserID: string; - srpSalt: string; - memLimit: number; - opsLimit: number; - kekSalt: string; - isEmailMFAEnabled: boolean; -} - -export interface GetSRPAttributesResponse { - attributes: SRPAttributes; -} - -export interface SRPSetupAttributes { - srpSalt: string; - srpVerifier: string; - srpUserID: string; - loginSubKey: string; -} - -export interface SetupSRPRequest { - srpUserID: string; - srpSalt: string; - srpVerifier: string; - srpA: string; -} - -export interface SetupSRPResponse { - setupID: string; - srpB: string; -} - -export interface CompleteSRPSetupRequest { - setupID: string; - srpM1: string; -} - -export interface CompleteSRPSetupResponse { - setupID: string; - srpM2: string; -} - -export interface CreateSRPSessionResponse { - sessionID: string; - srpB: string; -} - -export interface SRPVerificationResponse extends UserVerificationResponse { - srpM2: string; -} - -export interface UpdateSRPAndKeysRequest { - srpM1: string; - setupID: string; - updatedKeyAttr: UpdatedKey; - /** - * If true (default), then all existing sessions for the user will be - * invalidated. - */ - logOutOtherDevices?: boolean; -} - -export interface UpdateSRPAndKeysResponse { - srpM2: string; - setupID: string; -} - -export const getSRPAttributes = async ( - email: string, -): Promise => { - try { - const resp = await HTTPService.get( - await apiURL("/users/srp/attributes"), - { email }, - ); - return (resp.data as GetSRPAttributesResponse).attributes; - } catch (e) { - log.error("failed to get SRP attributes", e); - return null; - } -}; - -export const startSRPSetup = async ( - token: string, - setupSRPRequest: SetupSRPRequest, -): Promise => { - try { - const resp = await HTTPService.post( - await apiURL("/users/srp/setup"), - setupSRPRequest, - undefined, - { "X-Auth-Token": token }, - ); - - return resp.data as SetupSRPResponse; - } catch (e) { - log.error("failed to post SRP attributes", e); - throw e; - } -}; - -export const completeSRPSetup = async ( - token: string, - completeSRPSetupRequest: CompleteSRPSetupRequest, -) => { - try { - const resp = await HTTPService.post( - await apiURL("/users/srp/complete"), - completeSRPSetupRequest, - undefined, - { "X-Auth-Token": token }, - ); - return resp.data as CompleteSRPSetupResponse; - } catch (e) { - log.error("failed to complete SRP setup", e); - throw e; - } -}; - -export const createSRPSession = async (srpUserID: string, srpA: string) => { - const res = await fetch(await apiURL("/users/srp/create-session"), { - method: "POST", - headers: publicRequestHeaders(), - body: JSON.stringify({ srpUserID, srpA }), - }); - ensureOk(res); - const data = await res.json(); - // TODO: Use zod - return data as CreateSRPSessionResponse; -}; - -export const verifySRPSession = async ( - sessionID: string, - srpUserID: string, - srpM1: string, -) => { - try { - const resp = await HTTPService.post( - await apiURL("/users/srp/verify-session"), - { sessionID, srpUserID, srpM1 }, - undefined, - ); - return resp.data as SRPVerificationResponse; - } catch (e) { - log.error("verifySRPSession failed", e); - if ( - e instanceof ApiError && - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - e.httpStatusCode === HttpStatusCode.Unauthorized - ) { - throw Error(CustomError.INCORRECT_PASSWORD); - } else { - throw e; - } - } -}; - -export const updateSRPAndKeys = async ( - token: string, - updateSRPAndKeyRequest: UpdateSRPAndKeysRequest, -): Promise => { - try { - const resp = await HTTPService.post( - await apiURL("/users/srp/update"), - updateSRPAndKeyRequest, - undefined, - { "X-Auth-Token": token }, - ); - return resp.data as UpdateSRPAndKeysResponse; - } catch (e) { - log.error("updateSRPAndKeys failed", e); - throw e; - } -}; diff --git a/web/packages/accounts/services/srp.ts b/web/packages/accounts/services/srp.ts index fad07bb1a4..8618957f89 100644 --- a/web/packages/accounts/services/srp.ts +++ b/web/packages/accounts/services/srp.ts @@ -1,220 +1,662 @@ -import { sharedCryptoWorker } from "ente-base/crypto"; -import log from "ente-base/log"; -import { generateLoginSubKey } from "ente-shared/crypto/helpers"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; -import type { KeyAttributes } from "ente-shared/user/types"; +import { + deriveSubKeyBytes, + generateDeriveKeySalt, + toB64, +} from "ente-base/crypto"; +import { + authenticatedRequestHeaders, + ensureOk, + publicRequestHeaders, +} from "ente-base/http"; +import { apiURL } from "ente-base/origins"; import { SRP, SrpClient } from "fast-srp-hap"; import { v4 as uuidv4 } from "uuid"; +import { z } from "zod/v4"; import { - completeSRPSetup, - createSRPSession, - startSRPSetup, - verifySRPSession, - type SRPAttributes, - type SRPSetupAttributes, -} from "./srp-remote"; -import type { UserVerificationResponse } from "./user"; + RemoteSRPVerificationResponse, + type EmailOrSRPVerificationResponse, +} from "./user"; -const SRP_PARAMS = SRP.params["4096"]; +/** + * The SRP attributes for a user. + * + * These are created by a client on signup, saved to remote. During logins, they + * are fetched from remote. In both cases they are also persisted in local + * storage (both network ops and local storage use the same schema). + * + * [Note: SRP] + * + * The SRP (Secure Remote Password) protocol is a modified Diffie-Hellman key + * exchange that allows the remote to verify the user's possession of a + * password, and the user to ensure that remote is not being impersonated, + * without the password ever leaving the device. + * + * It is used as an (user selectable) alternative to email verification. + * + * For more about the what and why, see the announcement blog post + * https://ente.io/blog/ente-adopts-secure-remote-passwords/ + * + * Here we do not focus on the math (the above blog post links to reference + * material, and there is also an RFC), but instead of the various bits of + * information that get exchanged. + * + * Broadly, there are two scenarios: SRP setup, and SRP verification. + * + * [Note: SRP setup] ------------------- + * + * During SRP setup, client generates + * + * 01. A SRP user ID (a new UUID-v4) + * 02. A SRP password (deterministically derived from their regular KEK) + * 03. A SRP salt (randomly generated) + * + * These 3 things are enough to create a SRP verifier and client. The SRP client + * can just be thought of an ephemeral stateful mechanism to avoid passing all + * the state accrued so far to each operation. Each time when creating a SRP + * client, the app generates a new random secret and uses it during init. + * + * 04. Compute verifier = computeSRPVerifier({ userID, password, salt }) + * 05. Generates a new (ephemeral and random) clientSecret + * 06. Create client = new SRPClient({ userID, password, salt, clientSecret }) + * + * The client (app) then starts the setup ceremony with remote: + * + * 07. Use SRP client to conjure secret `a` and use that to compute a public A + * 08. Send { userID, salt, verifier, A } to remote ("/users/srp/setup") + * + * Remote then: + * + * 09. Generates a new (ephemeral and random) serverSecret + * 10. Saves { userID, serverSecret, A } into SRP sessions table + * 11. Creates server = new SRPServer({ verifier, serverSecret }) + * 12. Uses SRP server to conjure secret `b` and use that to compute a public B + * 13. Stashes { sessionID, userID, salt, verifier } into SRP setups table + * 14. Returns { setupID, B } to client + * + * Client then + * + * 15. Tells its SRP client about B + * 16. Computes SRP M1 (evidence message) using the SRP client + * 17. Sends { setupID, M1 } to remote ("/users/srp/complete") + * + * Remote then + * + * 18. Uses setupID to read the stashed { sessionID, verifier } + * 19. Uses sessionID to read { serverSecret, A } + * 20. Recreates server = new SRPServer({ verifier, serverSecret }) + * 21. Sets server.A + * 22. Verifies M1 using the SRP server, obtaining a SRP M2 (evidence message) + * 23. Returns M2 + * + * Client then + * + * 24. Verifies M2 + * + * SRP setup is now complete. + * + * A similar flow is used when the user changes their password. On password + * change, a new KEK is generated, thus the SRP password also changes, and so a + * subset of the steps above are done to update both client and remote. + * + * [Note: SRP verification] ----------------------- + * + * When the user is signing on a new device, the client + * + * 01. Fetches SRP attributes for a user to get { (SRP) userID, (SRP) salt } + * 02. Rederives SRP password from their KEK + * 03. Generates a new (ephemeral and random) clientSecret + * 04. Creates client = new SRPClient({ userID, password, salt, clientSecret }) + * 05. Uses SRP client to conjure secret `a` and use that to compute a public A + * 06. Sends { userID, A } to remote ("/users/srp/create-session") + * + * Remote + * + * 07. Retrieves { verifier } corresponding to the userID + * 08. Generates a new (ephemeral and random) serverSecret + * 09. Saves { userID, serverSecret, A } into SRP sessions table + * 10. Creates server = new SRPServer({ verifier, serverSecret }) + * 11. Sets server.A + * 12. Uses SRP server to conjure secret `b` and use that to compute a public B + * 13. Returns { sessionID, B } to client + * + * Client then + * + * 14. Sets client.B + * 15. Computes M1 (evidence message) + * 16. Sends { userID, sessionID, M1 } to remote ("/users/srp/verify-session") + * + * Remote + * + * 17. Retrieves { verifier } corresponding to the userID + * 17. Retrieves { serverSecret, A } using sessionID + * 18. Recreates server = new SRPServer({ verifier, serverSecret }) + * 19. Sets server.A + * 20. Verifies M1 using the SRP server, obtaining a SRP M2 (evidence message) + * 21. Returns M2 + * + * Client then + * + * 22. Verifies M2 + * + * SRP verification is now complete. + */ +export interface SRPAttributes { + /** + * The SRP user ID for the (Ente) user. + * + * Each Ente user gets a new randomly generated UUID v4 assigned as their + * SRP user ID during SRP setup. + */ + srpUserID: string; + /** + * The SRP salt. + * + * This is a randomly generated salt created for each SRP user during SRP + * setup. It is not meant to be secret. + */ + srpSalt: string; + /** + * The mem limit used during the KEK derivation from the password. + * + * See also the discussion in {@link kekSalt}. + */ + memLimit: number; + /** + * The ops limit used during the KEK derivation from the password. + * + * See also the discussion in {@link kekSalt}. + */ + opsLimit: number; + /** + * The salt used during the KEK derivation from the password. + * + * Base64 encoded. + * + * This is the same value as the {@link kekSalt} in {@link KeyAttributes}, + * made available by remote also as part of SRP attributes for convenience. + * See: [Note: KEK three tuple] for more details. + */ + kekSalt: string; + /** + * If true, then the client should use email verification instead of SRP. + */ + isEmailMFAEnabled: boolean; +} -export const configureSRP = async ({ - srpSalt, - srpUserID, - srpVerifier, - loginSubKey, -}: SRPSetupAttributes) => { - try { - const srpClient = await generateSRPClient( - srpSalt, - srpUserID, - loginSubKey, - ); +/** + * Zod schema for the {@link SRPAttributes} TypeScript type. + * + * We retain the SRP attributes response we get from remote verbatim when saving + * it to local storage, so the same schema describes both the remote type and + * the local storage type. + */ +const RemoteSRPAttributes = z.object({ + srpUserID: z.string(), + srpSalt: z.string(), + memLimit: z.number(), + opsLimit: z.number(), + kekSalt: z.string(), + isEmailMFAEnabled: z.boolean(), +}); - const srpA = convertBufferToBase64(srpClient.computeA()); - - log.debug(() => `srp a: ${srpA}`); - const token = getToken(); - const { setupID, srpB } = await startSRPSetup(token, { - srpA, - srpUserID, - srpSalt, - srpVerifier, - }); - - srpClient.setB(convertBase64ToBuffer(srpB)); - - const srpM1 = convertBufferToBase64(srpClient.computeM1()); - - const { srpM2 } = await completeSRPSetup(token, { srpM1, setupID }); - - srpClient.checkM2(convertBase64ToBuffer(srpM2)); - } catch (e) { - log.error("Failed to configure SRP", e); - throw e; - } +/** + * Fetch the {@link SRPAttributes} from remote for the Ente user with the + * provided email. + * + * Returns `undefined` if either there is no Ente user with the given + * {@link email}, or if there is a a user but they've not yet completed the SRP + * setup ceremony. + * + * @param email The email of the user whose SRP attributes we're fetching. + */ +export const getSRPAttributes = async ( + email: string, +): Promise => { + const res = await fetch(await apiURL("/users/srp/attributes", { email }), { + headers: publicRequestHeaders(), + }); + if (res.status == 404) return undefined; + ensureOk(res); + return z.object({ attributes: RemoteSRPAttributes }).parse(await res.json()) + .attributes; }; -export const generateSRPSetupAttributes = async ( - loginSubKey: string, -): Promise => { - const cryptoWorker = await sharedCryptoWorker(); +/** + * Return the user's {@link SRPAttributes} if they are present in local storage. + * + * Like key attributes, SRP attributes are also stored in the browser's local + * storage so will not be accessible to web workers. + */ +export const savedSRPAttributes = (): SRPAttributes | undefined => { + const jsonString = localStorage.getItem("srpAttributes"); + if (!jsonString) return undefined; + return RemoteSRPAttributes.parse(JSON.parse(jsonString)); +}; - const srpSalt = await cryptoWorker.generateSaltToDeriveKey(); +/** + * Save the user's {@link SRPAttributes} in local storage. + * + * Use {@link savedSRPAttributes} to retrieve them. + */ +export const saveSRPAttributes = (srpAttributes: SRPAttributes) => + localStorage.setItem("srpAttributes", JSON.stringify(srpAttributes)); + +/** + * A local-only structure holding information required for SRP setup. + * + * [Note: SRP setup attributes] + * + * In some cases, there might be a step between the client having access to the + * KEK (which we need for generating the SRP attributes, in particular the SRP + * password) and the time where the client can proceed with the SRP setup (which + * can only happen once we have an auth token). + * + * For example, when the user is signing up for a new account, the client has + * the KEK on the signup screen since the user just set their password, but then + * has to redirect to the screen for email verification, and it is only after + * email verification that the client obtains an auth token and the SRP setup + * can proceed (at which point it doesn't have access to the password and so + * cannot derive the KEK). + * + * This gap is not just about different screens, but since there is an email + * verification step involved, it might take time enough for the browser tab to + * get closed and reopened. So instead of keeping the attributes we need to + * continue with the SRP setup after email verification in memory, we + * temporarily stash them in local storage using an object that conforms to the + * following {@link SRPSetupAttributes} schema. + */ +const SRPSetupAttributes = z.object({ + srpUserID: z.string(), + srpSalt: z.string(), + srpVerifier: z.string(), + loginSubKey: z.string(), +}); + +export type SRPSetupAttributes = z.infer; + +/** + * Generate {@link SRPSetupAttributes} from the provided {@link kek}. + * + * @param kek The designated key encryption key (base64 encoded) for the user. + * This will be used to (deterministically) derive a SRP password. + */ +export const generateSRPSetupAttributes = async ( + kek: string, +): Promise => { + const loginSubKey = await deriveSRPLoginSubKey(kek); // Museum schema requires this to be a UUID. const srpUserID = uuidv4(); + const srpSalt = await generateDeriveKeySalt(); - const srpVerifierBuffer = SRP.computeVerifier( - SRP_PARAMS, - convertBase64ToBuffer(srpSalt), - Buffer.from(srpUserID), - convertBase64ToBuffer(loginSubKey), + const srpVerifier = bufferToB64( + SRP.computeVerifier( + SRP.params["4096"], + b64ToBuffer(srpSalt), + Buffer.from(srpUserID), + b64ToBuffer(loginSubKey), + ), ); - const srpVerifier = convertBufferToBase64(srpVerifierBuffer); + return { srpUserID, srpSalt, srpVerifier, loginSubKey }; +}; - const result = { srpUserID, srpSalt, srpVerifier, loginSubKey }; +/** + * Derive a "login sub-key" (which is really an arbitrary binary value, not + * human generated) for use as the SRP user password by applying a deterministic + * KDF (Key Derivation Function) to the provided {@link kek}. + * + * @param kek The user's KEK (key encryption key) as a base64 string. + * + * @returns A base64 encoded key that can be used as the SRP user password. + */ +const deriveSRPLoginSubKey = async (kek: string) => { + const kekSubKeyBytes = await deriveSubKeyBytes(kek, 32, 1, "loginctx"); + // Use the first 16 bytes (128 bits) of the KEK's KDF subkey as the SRP + // password (instead of entire 32 bytes). + return toB64(kekSubKeyBytes.slice(0, 16)); +}; - log.debug( - () => `SRP setup attributes generated: ${JSON.stringify(result)}`, +const b64ToBuffer = (base64: string) => Buffer.from(base64, "base64"); + +const bufferToB64 = (buffer: Buffer) => buffer.toString("base64"); + +/** + * Save {@link SRPSetupAttributes} in local storage for later use via + * {@link unstashAndUseSRPSetupAttributes}. + * + * See: [Note: SRP setup attributes] + */ +export const stashSRPSetupAttributes = ( + srpSetupAttributes: SRPSetupAttributes, +) => + localStorage.setItem( + "srpSetupAttributes", + JSON.stringify(srpSetupAttributes), ); - return result; +/** + * Retrieve the {@link SRPSetupAttributes}, if any, that were stashed by a + * previous call to {@link stashSRPSetupAttributes}. + * + * - If they are found, then invoke the provided callback ({@link cb}) with the + * value. If the promise returned by the callback fulfills, then remove the + * stashed value from local storage. + * + * - If they are not found, then the callback is not invoked. + */ +export const unstashAndUseSRPSetupAttributes = async ( + cb: (srpSetupAttributes: SRPSetupAttributes) => Promise, +) => { + const jsonString = localStorage.getItem("srpSetupAttributes"); + if (!jsonString) return; + const srpSetupAttributes = SRPSetupAttributes.parse(JSON.parse(jsonString)); + await cb(srpSetupAttributes); + localStorage.removeItem("srpSetupAttributes"); }; -export const loginViaSRP = async ( - srpAttributes: SRPAttributes, - kek: string, -): Promise => { - try { - const loginSubKey = await generateLoginSubKey(kek); - const srpClient = await generateSRPClient( - srpAttributes.srpSalt, - srpAttributes.srpUserID, - loginSubKey, - ); - const srpA = srpClient.computeA(); - const { srpB, sessionID } = await createSRPSession( - srpAttributes.srpUserID, - convertBufferToBase64(srpA), - ); - srpClient.setB(convertBase64ToBuffer(srpB)); +/** + * Use the provided {@link SRPSetupAttributes} to, well, setup SRP. + * + * See: [Note: SRP setup] + * + * @param srpSetupAttributes SRP setup attributes. + */ +export const setupSRP = async (srpSetupAttributes: SRPSetupAttributes) => + srpSetupOrReconfigure(srpSetupAttributes, completeSRPSetup); - const m1 = srpClient.computeM1(); - log.debug(() => `srp m1: ${convertBufferToBase64(m1)}`); - const { srpM2, ...rest } = await verifySRPSession( - sessionID, - srpAttributes.srpUserID, - convertBufferToBase64(m1), - ); - log.debug(() => `srp verify session successful,srpM2: ${srpM2}`); +/** + * A function that is called by {@link srpSetupOrReconfigure} to exchange the + * evidence message M1 for the evidence message M2 from remote. + * + * It is passed M1, and is expected to fulfill with M2. + */ +type SRPSetupOrReconfigureExchangeCallback = ({ + setupID, + srpM1, +}: { + setupID: string; + srpM1: string; +}) => Promise<{ srpM2: string }>; - srpClient.checkM2(convertBase64ToBuffer(srpM2)); +/** + * Use the provided {@link SRPSetupAttributes} to either setup (afresh) or + * reconfigure SRP (when the user changes their password). + * + * The flow (described in [Note: SRP setup]) is mostly the same except the tail + * end of the process where we exchange the evidence message M1 for the evidence + * message M2 from remote. To handle this variance, we provide a callback + * {@link exchangeCB}) that is invoked at this point in the sequence. + * + * @param srpSetupAttributes SRP setup attributes. + */ +const srpSetupOrReconfigure = async ( + { srpSalt, srpUserID, srpVerifier, loginSubKey }: SRPSetupAttributes, + exchangeCB: SRPSetupOrReconfigureExchangeCallback, +) => { + const srpClient = await generateSRPClient(srpSalt, srpUserID, loginSubKey); - log.debug(() => `srp server verify successful`); - return rest; - } catch (e) { - log.error("srp verify failed", e); - throw e; - } + const srpA = bufferToB64(srpClient.computeA()); + + const { setupID, srpB } = await startSRPSetup({ + srpUserID, + srpSalt, + srpVerifier, + srpA, + }); + + srpClient.setB(b64ToBuffer(srpB)); + + const srpM1 = bufferToB64(srpClient.computeM1()); + + const { srpM2 } = await exchangeCB({ srpM1, setupID }); + + srpClient.checkM2(b64ToBuffer(srpM2)); }; -// ==================== -// HELPERS -// ==================== - -export const generateSRPClient = async ( +const generateSRPClient = async ( srpSalt: string, srpUserID: string, loginSubKey: string, -) => { - return new Promise((resolve, reject) => { - SRP.genKey(function (err, secret1) { - try { - if (err) { - reject(err); - } - if (!secret1) { - throw Error("secret1 gen failed"); - } - const srpClient = new SrpClient( - SRP_PARAMS, - convertBase64ToBuffer(srpSalt), +) => + new Promise((resolve, reject) => { + SRP.genKey((err, clientKey) => { + if (err) reject(err); + resolve( + new SrpClient( + SRP.params["4096"], + b64ToBuffer(srpSalt), Buffer.from(srpUserID), - convertBase64ToBuffer(loginSubKey), - secret1, + b64ToBuffer(loginSubKey), + // The random `clientKey` parameterizes the current instance + // of the SRP client. + clientKey!, false, - ); - - resolve(srpClient); - } catch (e) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(e); - } + ), + ); }); }); -}; -export const convertBufferToBase64 = (buffer: Buffer) => { - return buffer.toString("base64"); -}; - -export const convertBase64ToBuffer = (base64: string) => { - return Buffer.from(base64, "base64"); -}; - -export async function generateKeyAndSRPAttributes( - passphrase: string, -): Promise<{ - keyAttributes: KeyAttributes; - masterKey: string; - srpSetupAttributes: SRPSetupAttributes; -}> { - const cryptoWorker = await sharedCryptoWorker(); - const masterKey = await cryptoWorker.generateEncryptionKey(); - const recoveryKey = await cryptoWorker.generateEncryptionKey(); - const kekSalt = await cryptoWorker.generateSaltToDeriveKey(); - const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt); - - const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64( - masterKey, - kek.key, - ); - const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64( - masterKey, - recoveryKey, - ); - const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64( - recoveryKey, - masterKey, - ); - - const keyPair = await cryptoWorker.generateKeyPair(); - const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64( - keyPair.privateKey, - masterKey, - ); - - const loginSubKey = await generateLoginSubKey(kek.key); - - const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey); - - const keyAttributes: KeyAttributes = { - kekSalt, - encryptedKey: masterKeyEncryptedWithKek.encryptedData, - keyDecryptionNonce: masterKeyEncryptedWithKek.nonce, - publicKey: keyPair.publicKey, - encryptedSecretKey: encryptedKeyPairAttributes.encryptedData, - secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce, - opsLimit: kek.opsLimit, - memLimit: kek.memLimit, - masterKeyEncryptedWithRecoveryKey: - masterKeyEncryptedWithRecoveryKey.encryptedData, - masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce, - recoveryKeyEncryptedWithMasterKey: - recoveryKeyEncryptedWithMasterKey.encryptedData, - recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce, - }; - - return { keyAttributes, masterKey, srpSetupAttributes }; +interface SetupSRPRequest { + srpUserID: string; + srpSalt: string; + srpVerifier: string; + srpA: string; } + +const SetupSRPResponse = z.object({ setupID: z.string(), srpB: z.string() }); + +type SetupSRPResponse = z.infer; + +/** + * Initiate SRP setup on remote. + * + * Part of the [Note: SRP setup] sequence. + */ +const startSRPSetup = async ( + setupSRPRequest: SetupSRPRequest, +): Promise => { + const res = await fetch(await apiURL("/users/srp/setup"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify(setupSRPRequest), + }); + ensureOk(res); + return SetupSRPResponse.parse(await res.json()); +}; + +interface CompleteSRPSetupRequest { + setupID: string; + srpM1: string; +} + +const CompleteSRPSetupResponse = z.object({ + setupID: z.string(), + srpM2: z.string(), +}); + +type CompleteSRPSetupResponse = z.infer; + +/** + * Complete a previously initiated SRP setup on remote. + * + * Part of the [Note: SRP setup] sequence. + */ +const completeSRPSetup = async ( + completeSRPSetupRequest: CompleteSRPSetupRequest, +) => { + const res = await fetch(await apiURL("/users/srp/complete"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify(completeSRPSetupRequest), + }); + ensureOk(res); + return CompleteSRPSetupResponse.parse(await res.json()); +}; + +/** + * The subset of {@link KeyAttributes} that get updated when the user changes + * their password. + */ +export interface UpdatedKeyAttr { + kekSalt: string; + encryptedKey: string; + keyDecryptionNonce: string; + opsLimit: number; + memLimit: number; +} + +/** + * Update the user's affected key and SRP attributes when they change their + * password. + * + * The flow on changing password is similar to the flow on initial SRP setup, + * with some differences at the tail end of the flow. See: [Note: SRP setup]. + * + * @param srpSetupAttributes Attributes for the user's updated SRP setup. + * + * @param updatedKeyAttr The subset of the user's key attributes which need to + * be updated to reflect their changed password. + */ +export const updateSRPAndKeyAttributes = ( + srpSetupAttributes: SRPSetupAttributes, + updatedKeyAttr: UpdatedKeyAttr, +) => + srpSetupOrReconfigure(srpSetupAttributes, ({ setupID, srpM1 }) => + updateSRPAndKeys({ setupID, srpM1, updatedKeyAttr }), + ); + +export interface UpdateSRPAndKeysRequest { + setupID: string; + srpM1: string; + updatedKeyAttr: UpdatedKeyAttr; + /** + * If true (default), then all existing sessions for the user will be + * invalidated. + */ + logOutOtherDevices?: boolean; +} + +const UpdateSRPAndKeysResponse = z.object({ + srpM2: z.string(), + setupID: z.string(), +}); + +type UpdateSRPAndKeysResponse = z.infer; + +/** + * Update the SRP attributes and a subset of the key attributes on remote. + * + * This is invoked during the flow when the user changes their password, and SRP + * needs to be reconfigured. See: [Note: SRP setup]. + */ +const updateSRPAndKeys = async ( + updateSRPAndKeysRequest: UpdateSRPAndKeysRequest, +): Promise => { + const res = await fetch(await apiURL("/users/srp/update"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify(updateSRPAndKeysRequest), + }); + ensureOk(res); + return UpdateSRPAndKeysResponse.parse(await res.json()); +}; + +/** + * The message of the {@link Error} that is thrown by {@link verifySRP} if + * remote fails SRP verification with a HTTP 401. + * + * The API contract allows for a SRP verification 401 both because of incorrect + * credentials or a non existent account. + */ +export const srpVerificationUnauthorizedErrorMessage = + "SRP verification failed (HTTP 401 Unauthorized)"; + +/** + * Log the user in to a new device by performing SRP verification. + * + * This function implements the flow described in [Note: SRP verification]. + * + * @param srpAttributes The user's SRP attributes. + * + * @param kek The user's key encryption key as a base64 string. + * + * @returns If SRP verification is successful, it returns a + * {@link EmailOrSRPVerificationResponse}. + * + * @throws An Error with {@link srpVerificationUnauthorizedErrorMessage} in case + * there is no such account, or if the credentials (kek) are incorrect. + */ +export const verifySRP = async ( + { srpUserID, srpSalt }: SRPAttributes, + kek: string, +): Promise => { + const loginSubKey = await deriveSRPLoginSubKey(kek); + const srpClient = await generateSRPClient(srpSalt, srpUserID, loginSubKey); + + // Send A, obtain B. + const { srpB, sessionID } = await createSRPSession({ + srpUserID, + srpA: bufferToB64(srpClient.computeA()), + }); + + srpClient.setB(b64ToBuffer(srpB)); + + // Send M1, obtain M2. + const { srpM2, ...rest } = await verifySRPSession({ + sessionID, + srpUserID, + srpM1: bufferToB64(srpClient.computeM1()), + }); + + srpClient.checkM2(b64ToBuffer(srpM2)); + + return rest; +}; + +interface CreateSRPSessionRequest { + srpUserID: string; + srpA: string; +} + +const CreateSRPSessionResponse = z.object({ + sessionID: z.string(), + srpB: z.string(), +}); + +type CreateSRPSessionResponse = z.infer; + +const createSRPSession = async ( + createSRPSessionRequest: CreateSRPSessionRequest, +): Promise => { + const res = await fetch(await apiURL("/users/srp/create-session"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify(createSRPSessionRequest), + }); + ensureOk(res); + return CreateSRPSessionResponse.parse(await res.json()); +}; + +interface VerifySRPSessionRequest { + sessionID: string; + srpUserID: string; + srpM1: string; +} + +type SRPVerificationResponse = z.infer; + +const verifySRPSession = async ( + verifySRPSessionRequest: VerifySRPSessionRequest, +): Promise => { + const res = await fetch(await apiURL("/users/srp/verify-session"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify(verifySRPSessionRequest), + }); + if (res.status == 401) { + throw new Error(srpVerificationUnauthorizedErrorMessage); + } + ensureOk(res); + return RemoteSRPVerificationResponse.parse(await res.json()); +}; diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index e37e66a189..398eec1372 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -1,73 +1,482 @@ +import { + generateSRPSetupAttributes, + getSRPAttributes, + saveSRPAttributes, + updateSRPAndKeyAttributes, + type UpdatedKeyAttr, +} from "ente-accounts/services/srp"; +import { + decryptBox, + deriveInteractiveKey, + deriveSensitiveKey, + encryptBox, + generateKey, + generateKeyPair, +} from "ente-base/crypto"; +import { isDevBuild } from "ente-base/env"; import { authenticatedRequestHeaders, ensureOk, publicRequestHeaders, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; -import HTTPService from "ente-shared/network/HTTPService"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; -import type { KeyAttributes } from "ente-shared/user/types"; +import { + ensureMasterKeyFromSession, + saveMasterKeyInSessionAndSafeStore, +} from "ente-base/session"; +import { getAuthToken } from "ente-base/token"; +import { getData, setLSUser } from "ente-shared/storage/localStorage"; +import { ensure } from "ente-utils/ensure"; import { nullToUndefined } from "ente-utils/transform"; -import { z } from "zod"; +import { z } from "zod/v4"; +import { getUserRecoveryKey, recoveryKeyFromMnemonic } from "./recovery-key"; -export interface UserVerificationResponse { +export interface User { id: number; - keyAttributes?: KeyAttributes | undefined; - encryptedToken?: string | undefined; - token?: string; - twoFactorSessionID?: string | undefined; - passkeySessionID?: string | undefined; + email: string; + token: string; + encryptedToken: string; + isTwoFactorEnabled: boolean; + twoFactorSessionID: string; +} + +/** + * The local storage data about the user after they've logged in. + */ +const LocalUser = z.object({ /** - * Base URL for the accounts app where we should redirect to for passkey - * verification. + * The user's ID. + */ + id: z.number(), + /** + * The user's email. + */ + email: z.string(), + /** + * The user's (plaintext) auth token. * - * This will only be set if the user has setup a passkey (i.e., whenever - * {@link passkeySessionID} is defined). + * It is used for making API calls on their behalf, by passing this token as + * the value of the X-Auth-Token header in the HTTP request. + * + * Deprecated, use `getAuthToken()` instead (which fetches it from IDB). */ - accountsUrl: string | undefined; - /** - * If both passkeys and TOTP based two factors are enabled, then {@link - * twoFactorSessionIDV2} will be set to the TOTP session ID instead of - * {@link twoFactorSessionID}. - */ - twoFactorSessionIDV2?: string | undefined; - srpM2?: string | undefined; -} - -export interface TwoFactorVerificationResponse { - id: number; - keyAttributes: KeyAttributes; - encryptedToken?: string; - token?: string; -} - -const TwoFactorSecret = z.object({ - secretCode: z.string(), - qrCode: z.string(), + token: z.string(), }); -export type TwoFactorSecret = z.infer; +/** + * The local storage data about the user after they've logged in. + */ +export type LocalUser = z.infer; -export interface TwoFactorRecoveryResponse { - encryptedSecret: string; - secretDecryptionNonce: string; -} +/** + * The local storage data about the user before login or signup is complete. + * + * [Note: Partial local user] + * + * During login or signup, the user object exists in various partial states in + * local storage. + * + * - Initially, there is no user object in local storage. + * + * - When the user enters their email, the email property of the stored object + * is set, but nothing else. + * + * - After they verify their password, we have two cases: if second factor + * verification is not set, and when it is set. + * + * - If second factor verification is not set, then after verifying their + * password their {@link id} and {@link encryptedToken} will get filled in, + * and {@link isTwoFactorEnabled} will be set to false. + * + * - If they have second factor verification set, then after verifying their + * password {@link isTwoFactorEnabled} and {@link twoFactorSessionID} will + * also get filled in. Once they verify their TOTP based second factor, their + * {@link id} and {@link encryptedToken} will also get filled in. + * + * So while the underlying storage is the same, we offer two APIs for code to + * obtain the user: + * + * - Before login is complete, or when it is unknown if login is complete or + * not, then {@link partialLocalUser} can be used to obtain a + * {@link LocalUser} with all of its properties set to be optional. + * + * - When we know that the login has completed, we can use either + * {@link localUser} (which returns `undefined` if our presumption is false) + * or {@link ensureLocalUser} (which throws if our presumption is false) to + * obtain an object with all the properties expected to be present for a + * locally persisted user set to be required. + */ +export const partialLocalUser = (): Partial | undefined => { + // TODO: duplicate of getData("user") + const s = localStorage.getItem("user"); + if (!s) return undefined; + return LocalUser.partial().parse(JSON.parse(s)); +}; -export interface UpdatedKey { - kekSalt: string; +/** + * Save the users data as we accrue it during the signup or login flow. + * + * See: [Note: Partial local user]. + * + * TODO: WARNING: This does not update the KV token. The idea is to gradually + * move over uses of setLSUser to this while explicitly setting the KV token + * where needed. + */ +export const savePartialLocalUser = (partialLocalUser: Partial) => + localStorage.setItem("user", JSON.stringify(partialLocalUser)); + +/** + * Return the logged-in user, if someone is indeed logged in. Otherwise return + * `undefined`. + * + * The user's data is stored in the browser's localStorage. Thus, this function + * only works from the main thread, not from web workers (local storage is not + * accessible to web workers). + */ +export const localUser = (): LocalUser | undefined => { + // TODO: duplicate of getData("user") + const s = localStorage.getItem("user"); + if (!s) return undefined; + const { success, data } = LocalUser.safeParse(JSON.parse(s)); + return success ? data : undefined; +}; + +/** + * A wrapper over {@link localUser} with that throws if no one is logged in. + */ +export const ensureLocalUser = (): LocalUser => + ensureExpectedLoggedInValue(localUser()); + +/** + * A function throws an error if a value that is expected to be truthy when the + * user is logged in is instead falsey. + * + * This is meant as a convenience wrapper to assert that a value we expect when + * the user is logged in is indeed there. + */ +export const ensureExpectedLoggedInValue = (t: T | undefined): T => { + if (!t) throw new Error("Not logged in"); + return t; +}; + +/** + * The user's various encrypted keys and their related attributes. + * + * - Attributes to derive the KEK, the (master) key encryption key. + * - Encrypted master key (with KEK) + * - Encrypted master key (with recovery key) + * - Encrypted recovery key (with master key). + * - Public key and encrypted private key (with master key). + * + * The various "key" attributes are base64 encoded representations of the + * underlying binary data. + * + * [Note: Key attribute mutability] + * + * The key attributes contain two subsets: + * + * - Attributes that changes when the user changes their password. These are the + * {@link UpdatedKeyAttr}. + * + * - All other attributes never change after initial setup. + */ +export interface KeyAttributes { + /** + * The user's master key encrypted with the key encryption key. + * + * Base64 encoded. + * + * [Note: Key encryption key] + * + * The user's master key is encrypted with a "key encryption key" (lovingly + * called a "KEK" sometimes). + * + * The KEK itself is derived from the user's password. + * + * 1. User enters password on new device. + * + * 2. Client derives KEK from this password (using the {@link kekSalt}, + * {@link opsLimit} and {@link memLimit} as parameters for the + * derivation). + * + * 3. Client use KEK to decrypt the master key from {@link encryptedKey} and + * {@link keyDecryptionNonce}. + */ encryptedKey: string; + /** + * The nonce used during the encryption of the master key. + * + * Base64 encoded. + * + * @see {@link encryptedKey}. + */ keyDecryptionNonce: string; - memLimit: number; + /** + * The salt used during the derivation of the KEK. + * + * Base64 encoded. + * + * [Note: KEK three tuple] + * + * The three tuple (kekSalt, opsLimit, memLimit) is needed (along with the + * user's password) to rederive the KEK when the user logs in on a new + * client (See: [Note: Key encryption key]). + * + * The client can obtain these three by fetching their key attributes from + * remote, however unless {@link isEmailMFAEnabled} is enabled (which is not + * by default), then the user's credentials are verified using SRP instead + * of email verification. So as a convenience for this (majority) flow, + * remote also provides this exact same three tuple as part of the + * {@link SRPAttributes} fetched from remote. + * + * So on remote the KEK three tuple is the same whether it be part of key + * attributes or SRP attributes. When the user changes their password, both + * of them also get updated simulataneously (they use the same storage). + * + * However, on the client side these two sets of three tuples might diverge + * because of the client generating interactive key attributes. When that + * happens, the locally saved key attributes will be overwritten by the KEK + * three tuple for the new generated interactive KEK parameters, while the + * SRP attributes will continue to reflect the "original" KEK three tuple we + * got from remote. + */ + kekSalt: string; + /** + * The operation limit used during the derivation of the KEK. + * + * The {@link opsLimit} and {@link memLimit} are complementary parameters + * that define the amount of work done by the key derivation function. See + * the {@link deriveKey}, {@link deriveSensitiveKey} and + * {@link deriveInteractiveKey} functions for more detail about them. + * + * See: [Note: Key encryption key]. + */ opsLimit: number; + /** + * The memory limit used during the derivation of the KEK. + * + * See {@link opsLimit} for more details. + */ + memLimit: number; + /** + * The user's public key (part of their public-key keypair, the other half + * being the {@link encryptedSecretKey}). + * + * Base64 encoded. + */ + publicKey: string; + /** + * The user's private key (part of their public-key keypair, the other half + * being the {@link publicKey}) encrypted with their master key. + * + * Base64 encoded. + * + * [Note: privateKey and secretKey] + * + * The nomenclature for the key pair follows libsodium's conventions + * (https://doc.libsodium.org/public-key_cryptography/authenticated_encryption#key-pair-generation), + * who possibly chose public + secret instead of public + private to avoid + * confusion with shorthand notation (pk). + * + * However, the library author later changed their mind on this, so while + * libsodium itself (the C library) and the documentation uses "secretKey", + * the JavaScript implementation (libsodium.js) uses "privateKey". + * + * This structure uses the term "secretKey" since that is what the remote + * protocol already was based on. Within the web app codebase, we use + * "privateKey" since that is what the underlying libsodium.js uses. + */ + encryptedSecretKey: string; + /** + * The nonce used during the encryption of {@link encryptedSecretKey}. + */ + secretKeyDecryptionNonce: string; + /** + * The user's master key after being encrypted with their recovery key. + * + * Base64 encoded. + * + * This allows the user to recover their master key if they forget their + * password but still have their recovery key. + * + * Note: This value doesn't change after being initially created. + */ + masterKeyEncryptedWithRecoveryKey?: string; + /** + * The nonce used during the encryption of + * {@link masterKeyEncryptedWithRecoveryKey}. + * + * Base64 encoded. + */ + masterKeyDecryptionNonce?: string; + /** + * The user's recovery key after being encrypted with their master key. + * + * Base64 encoded. + * + * Note: This value doesn't change after being initially created. + */ + recoveryKeyEncryptedWithMasterKey?: string; + /** + * The nonce used during the encryption of + * {@link recoveryKeyEncryptedWithMasterKey}. + * + * Base64 encoded. + */ + recoveryKeyDecryptionNonce?: string; } -export interface RecoveryKey { +/** + * Zod schema for {@link KeyAttributes}. + */ +export const RemoteKeyAttributes = z.object({ + kekSalt: z.string(), + encryptedKey: z.string(), + keyDecryptionNonce: z.string(), + publicKey: z.string(), + encryptedSecretKey: z.string(), + secretKeyDecryptionNonce: z.string(), + memLimit: z.number(), + opsLimit: z.number(), + masterKeyEncryptedWithRecoveryKey: z + .string() + .nullish() + .transform(nullToUndefined), + masterKeyDecryptionNonce: z.string().nullish().transform(nullToUndefined), + recoveryKeyEncryptedWithMasterKey: z + .string() + .nullish() + .transform(nullToUndefined), + recoveryKeyDecryptionNonce: z.string().nullish().transform(nullToUndefined), +}); + +/** + * Return the user's {@link KeyAttributes} if they are present in local storage. + * + * The key attributes are stored in the browser's localStorage. Thus, this + * function only works from the main thread, not from web workers (local storage + * is not accessible to web workers). + */ +export const savedKeyAttributes = (): KeyAttributes | undefined => { + const jsonString = localStorage.getItem("keyAttributes"); + if (!jsonString) return undefined; + return RemoteKeyAttributes.parse(JSON.parse(jsonString)); +}; + +/** + * A variant of {@link savedKeyAttributes} that throws if keyAttributes are not + * present in local storage. + */ +export const ensureSavedKeyAttributes = (): KeyAttributes => + ensureExpectedLoggedInValue(savedKeyAttributes()); + +/** + * Save the user's {@link KeyAttributes} in local storage. + * + * Use {@link savedKeyAttributes} to retrieve them. + */ +export const saveKeyAttributes = (keyAttributes: KeyAttributes) => + localStorage.setItem("keyAttributes", JSON.stringify(keyAttributes)); + +export interface GenerateKeysAndAttributesResult { + masterKey: string; + kek: string; + keyAttributes: KeyAttributes; +} + +/** + * Generate a new set of key attributes. + * + * @param password The password to use for deriving the key encryption key. + * + * @returns a newly generated master key (base64 string), kek (base64 string) + * and the key attributes associated with the combination. + */ +export async function generateKeysAndAttributes( + password: string, +): Promise { + const masterKey = await generateKey(); + const recoveryKey = await generateKey(); + const { + key: kek, + salt: kekSalt, + opsLimit, + memLimit, + } = await deriveSensitiveKey(password); + + const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } = + await encryptBox(masterKey, kek); + const { + encryptedData: masterKeyEncryptedWithRecoveryKey, + nonce: masterKeyDecryptionNonce, + } = await encryptBox(masterKey, recoveryKey); + const { + encryptedData: recoveryKeyEncryptedWithMasterKey, + nonce: recoveryKeyDecryptionNonce, + } = await encryptBox(recoveryKey, masterKey); + + const keyPair = await generateKeyPair(); + const { + encryptedData: encryptedSecretKey, + nonce: secretKeyDecryptionNonce, + } = await encryptBox(keyPair.privateKey, masterKey); + + const keyAttributes: KeyAttributes = { + encryptedKey, + keyDecryptionNonce, + kekSalt, + opsLimit, + memLimit, + publicKey: keyPair.publicKey, + encryptedSecretKey, + secretKeyDecryptionNonce, + masterKeyEncryptedWithRecoveryKey, + masterKeyDecryptionNonce, + recoveryKeyEncryptedWithMasterKey, + recoveryKeyDecryptionNonce, + }; + + return { masterKey, kek, keyAttributes }; +} + +/** + * Update or set the user's {@link KeyAttributes} on remote. + */ +export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) => + ensureOk( + await fetch(await apiURL("/users/attributes"), { + method: "PUT", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify({ keyAttributes }), + }), + ); + +export interface RecoveryKeyAttributes { masterKeyEncryptedWithRecoveryKey: string; masterKeyDecryptionNonce: string; recoveryKeyEncryptedWithMasterKey: string; recoveryKeyDecryptionNonce: string; } +/** + * Update the encrypted recovery key attributes for the logged in user. + * + * In practice, this is not expected to be called and is meant as a rare + * fallback for very old accounts created prior to recovery key related + * attributes being assigned on account setup. Even for these, it'll be called + * only once. + */ +export const putUserRecoveryKeyAttributes = async ( + recoveryKeyAttributes: RecoveryKeyAttributes, +) => + ensureOk( + await fetch(await apiURL("/users/recovery-key"), { + method: "PUT", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify(recoveryKeyAttributes), + }), + ); + /** * Ask remote to send a OTP / OTT to the given email to verify that the user has * access to it. Subsequent the app will pass this OTT back via the @@ -94,6 +503,134 @@ export const sendOTT = async ( }), ); +/** + * The response from remote on a successful user verification, either via + * {@link verifyEmail} or {@link verifySRP}. + * + * The {@link id} is always present. The rest of the values are are optional + * since only a subset of them will be returned depending on the case: + * + * 1. If the user has both passkeys and TOTP based second factor enabled, then + * the following will be set: + * - {@link passkeySessionID}, {@link accountsUrl} + * - {@link twoFactorSessionIDV2} + * + * 2. If the user has only passkeys enabled, then the following will be set: + * - {@link passkeySessionID}, {@link accountsUrl} + * + * 3. If the user has only TOTP based second factor enabled, then the following + * will be set: + * - {@link twoFactorSessionID} + * + * 4. If the user doesn't have any second factor, but has already setup their + * key attributes, then the following will be set: + * - {@link keyAttributes} + * - {@link encryptedToken} + * + * 5. Finally, in the rare case that the user has not yet setup their key + * attributes, then the following will be set: + * - {@link token} + */ +export interface EmailOrSRPVerificationResponse { + /** + * The user's ID. + */ + id: number; + /** + * The user's key attributes. + * + * These will be set (along with the {@link encryptedToken}) if the user + * does not have a second factor. + */ + keyAttributes?: KeyAttributes; + /** + * The base64 representation of an encrypted auth token, encrypted using the + * user's public key. + * + * These will be set (along with the {@link keyAttributes}) if the user + * does not have a second factor. + */ + encryptedToken?: string; + /** + * The base64 representation of an auth token. + * + * This will be set in the rare edge case for when the user has not yet + * setup their key attributes. + */ + token?: string; + /** + * A session ID that can be used to complete the TOTP based second factor. + * + * This will be set if the user has enabled a TOTP based second factor but + * has not enabled passkeys. + */ + twoFactorSessionID?: string; + /** + * A session ID that can be used to complete passkey verification. + * + * This will be set if the user has added a passkey to their account. + */ + passkeySessionID?: string; + /** + * Base URL for the accounts app where we should redirect to for passkey + * verification. + * + * This will only be set if the user has setup a passkey (i.e., whenever + * {@link passkeySessionID} is defined). + */ + accountsUrl?: string; + /** + * A session ID that can be used to complete the TOTP based second fator. + * + * This will be set in lieu of {@link twoFactorSessionID} if the user has + * setup both passkeys and TOTP based two factors are enabled for their + * account. + * + * --- + * + * Historical context: {@link twoFactorSessionIDV2} is only set if user has + * both passkey and two factor enabled. This is to ensure older clients keep + * using passkey flow when both are set. It is intended to be removed once + * all clients starts surfacing both options for performing 2FA. + * + * See also {@link useSecondFactorChoiceIfNeeded}. + */ + twoFactorSessionIDV2?: string; +} + +/** + * Zod schema for the {@link EmailOrSRPVerificationResponse} type. + * + * See: [Note: Duplicated Zod schema and TypeScript type] + */ +const RemoteEmailOrSRPVerificationResponse = z.object({ + id: z.number(), + keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined), + encryptedToken: z.string().nullish().transform(nullToUndefined), + token: z.string().nullish().transform(nullToUndefined), + twoFactorSessionID: z.string().nullish().transform(nullToUndefined), + passkeySessionID: z.string().nullish().transform(nullToUndefined), + accountsUrl: z.string().nullish().transform(nullToUndefined), + twoFactorSessionIDV2: z.string().nullish().transform(nullToUndefined), +}); + +/** + * A specialization of {@link RemoteEmailOrSRPVerificationResponse} for SRP + * verification, which results in the {@link srpM2} field in addition to the + * other ones. + * + * The declaration conceptually belongs to `srp.ts`, but is here to avoid cyclic + * dependencies. + */ +export const RemoteSRPVerificationResponse = z.object({ + ...RemoteEmailOrSRPVerificationResponse.shape, + /** + * The SRP M2 (evidence message), the proof that the server has the + * verifier. + */ + srpM2: z.string(), +}); + /** * Verify user's access to the given {@link email} by comparing the OTT that * remote previously sent to that email. @@ -110,110 +647,21 @@ export const verifyEmail = async ( email: string, ott: string, source: string | undefined, -): Promise => { +): Promise => { const res = await fetch(await apiURL("/users/verify-email"), { method: "POST", headers: publicRequestHeaders(), - body: JSON.stringify({ email, ott, ...(source ? { source } : {}) }), + body: JSON.stringify({ email, ott, ...(source && { source }) }), }); ensureOk(res); - // See: [Note: strict mode migration] - // - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return EmailOrSRPAuthorizationResponse.parse(await res.json()); + return RemoteEmailOrSRPVerificationResponse.parse(await res.json()); }; -/** - * Zod schema for {@link KeyAttributes}. - */ -const RemoteKeyAttributes = z.object({ - kekSalt: z.string(), - encryptedKey: z.string(), - keyDecryptionNonce: z.string(), - publicKey: z.string(), - encryptedSecretKey: z.string(), - secretKeyDecryptionNonce: z.string(), - memLimit: z.number(), - opsLimit: z.number(), - masterKeyEncryptedWithRecoveryKey: z - .string() - .nullish() - .transform(nullToUndefined), - masterKeyDecryptionNonce: z.string().nullish().transform(nullToUndefined), - recoveryKeyEncryptedWithMasterKey: z - .string() - .nullish() - .transform(nullToUndefined), - recoveryKeyDecryptionNonce: z.string().nullish().transform(nullToUndefined), -}); - -/** - * Zod schema for response from remote on a successful user verification, either - * via {@link verifyEmail} or {@link verifySRPSession}. - * - * If a second factor is enabled than one of the two factor session IDs - * (`passkeySessionID`, `twoFactorSessionID` / `twoFactorSessionIDV2`) will be - * set. Otherwise `keyAttributes` and `encryptedToken` will be set. - */ -export const EmailOrSRPAuthorizationResponse = z.object({ - id: z.number(), - keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined), - encryptedToken: z.string().nullish().transform(nullToUndefined), - token: z.string().nullish().transform(nullToUndefined), - twoFactorSessionID: z.string().nullish().transform(nullToUndefined), - passkeySessionID: z.string().nullish().transform(nullToUndefined), - // Base URL for the accounts app where we should redirect to for passkey - // verification. - accountsUrl: z.string().nullish().transform(nullToUndefined), - // TwoFactorSessionIDV2 is only set if user has both passkey and two factor - // enabled. This is to ensure older clients keep using passkey flow when - // both are set. It is intended to be removed once all clients starts - // surfacing both options for performing 2FA. - // - // See `useSecondFactorChoiceIfNeeded`. - twoFactorSessionIDV2: z.string().nullish().transform(nullToUndefined), - // srpM2 is sent only if the user is logging via SRP. It is is the SRP M2 - // value aka the proof that the server has the verifier. - srpM2: z.string().nullish().transform(nullToUndefined), -}); - -/** - * The result of a successful two factor verification (totp or passkey). - */ -export const TwoFactorAuthorizationResponse = z.object({ - id: z.number(), - /** TODO: keyAttributes is guaranteed to be returned by museum, update the - * types to reflect that. */ - keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined), - /** TODO: encryptedToken is guaranteed to be returned by museum, update the - * types to reflect that. */ - encryptedToken: z.string().nullish().transform(nullToUndefined), -}); - -export type TwoFactorAuthorizationResponse = z.infer< - typeof TwoFactorAuthorizationResponse ->; - -export const putAttributes = async ( - token: string, - keyAttributes: KeyAttributes, -) => - HTTPService.put( - await apiURL("/users/attributes"), - { keyAttributes }, - undefined, - { "X-Auth-Token": token }, - ); - /** * Log the user out on remote, if possible and needed. */ export const remoteLogoutIfNeeded = async () => { - let headers: HeadersInit; - try { - headers = await authenticatedRequestHeaders(); - } catch { + if (!(await getAuthToken())) { // If the logout is attempted during the signup flow itself, then we // won't have an auth token. return; @@ -221,7 +669,7 @@ export const remoteLogoutIfNeeded = async () => { const res = await fetch(await apiURL("/users/logout"), { method: "POST", - headers, + headers: await authenticatedRequestHeaders(), }); if (res.status == 401) { // Ignore if we get a 401 Unauthorized, this is expected to happen on @@ -232,58 +680,164 @@ export const remoteLogoutIfNeeded = async () => { ensureOk(res); }; -export const verifyTwoFactor = async (code: string, sessionID: string) => { - const res = await fetch(await apiURL("/users/two-factor/verify"), { - method: "POST", - headers: publicRequestHeaders(), - body: JSON.stringify({ code, sessionID }), - }); - ensureOk(res); - const json = await res.json(); - // TODO: Use zod here - return json as UserVerificationResponse; -}; +/** + * Generate a new local-only KEK (key encryption key) suitable for interactive + * use and update the locally saved key attributes to reflect it. + * + * See {@link deriveInteractiveKey} for more details. + * + * In brief, after the initial password verification, we create a new + * inetractive KEK derived from the same password as the original KEK, but with + * so called interactive mem and ops limits which result in a noticeably faster + * key derivation. + * + * We then overwrite the encrypted master key, encryption nonce and the KEK + * derivation parameters (see: [Note: KEK three tuple]) in the locally persisted + * {@link KeyAttributes} so that these interactive parameters get used + * subsequent reauthentication. + * + * These are more ergonomic for the user especially in the web app where they + * need to enter their password to access their masterKey when repopening the + * app in a new tab (on desktop we can avoid this by using OS storage, see + * [Note: Safe storage and interactive KEK attributes]). + * + * @param password The user's password. + * + * @param keyAttributes The existing "original" key attributes, which we + * might've generated locally (new signup) or fetched from remote (existing + * login). + * + * @param masterKey The user's master key (base64 encoded). + * + * @returns the update key attributes. + */ +export const generateAndSaveInteractiveKeyAttributes = async ( + password: string, + keyAttributes: KeyAttributes, + key: string, +): Promise => { + const { + key: interactiveKEK, + salt: kekSalt, + opsLimit, + memLimit, + } = await deriveInteractiveKey(password); -/** The type of the second factor we're trying to act on */ -export type TwoFactorType = "totp" | "passkey"; + const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } = + await encryptBox(key, interactiveKEK); -export const recoverTwoFactor = async ( - sessionID: string, - twoFactorType: TwoFactorType, -) => { - const resp = await HTTPService.get( - await apiURL("/users/two-factor/recover"), - { sessionID, twoFactorType }, - ); - return resp.data as TwoFactorRecoveryResponse; -}; - -export const removeTwoFactor = async ( - sessionID: string, - secret: string, - twoFactorType: TwoFactorType, -) => { - const resp = await HTTPService.post( - await apiURL("/users/two-factor/remove"), - { sessionID, secret, twoFactorType }, - ); - return resp.data as TwoFactorVerificationResponse; -}; - -export const changeEmail = async (email: string, ott: string) => { - await HTTPService.post( - await apiURL("/users/change-email"), - { email, ott }, - undefined, - { "X-Auth-Token": getToken() }, - ); + const interactiveKeyAttributes = { + ...keyAttributes, + encryptedKey, + keyDecryptionNonce, + kekSalt, + opsLimit, + memLimit, + }; + saveKeyAttributes(interactiveKeyAttributes); + return interactiveKeyAttributes; }; /** - * Start the two factor setup process by fetching a secret code (and the - * corresponding QR code) from remote. + * Change the email associated with the user's account (both locally and on + * remote) + * + * @param email The new email. + * + * @param ott The verification code that was sent to the new email. */ -export const setupTwoFactor = async () => { +export const changeEmail = async (email: string, ott: string) => { + await postChangeEmail(email, ott); + await setLSUser({ ...getData("user"), email }); +}; + +/** + * Change the email associated with the user's account on remote. + */ +const postChangeEmail = async (email: string, ott: string) => + ensureOk( + await fetch(await apiURL("/users/change-email"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify({ email, ott }), + }), + ); + +/** + * Change the user's password on both remote and locally. + * + * @param password The new password. + */ +export const changePassword = async (password: string) => { + const user = ensureLocalUser(); + const masterKey = await ensureMasterKeyFromSession(); + const keyAttributes = ensureSavedKeyAttributes(); + + // Generate new KEK. + const { + key: kek, + salt: kekSalt, + opsLimit, + memLimit, + } = await deriveSensitiveKey(password); + + // Generate new key attributes. + const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } = + await encryptBox(masterKey, kek); + const updatedKeyAttr: UpdatedKeyAttr = { + encryptedKey, + keyDecryptionNonce, + kekSalt, + opsLimit, + memLimit, + }; + + // Update SRP and key attributes on remote. + await updateSRPAndKeyAttributes( + await generateSRPSetupAttributes(kek), + updatedKeyAttr, + ); + + // Update SRP attributes locally. + const srpAttributes = await getSRPAttributes(user.email); + saveSRPAttributes(ensure(srpAttributes)); + + // Update key attributes locally, generating a new interactive kek while + // we're at it. + await generateAndSaveInteractiveKeyAttributes( + password, + { ...keyAttributes, ...updatedKeyAttr }, + masterKey, + ); + + // TODO(RE): This shouldn't be needed, remove me. As a soft remove, + // disabling it for dev builds. (tag: Migration) + if (!isDevBuild) { + await saveMasterKeyInSessionAndSafeStore(masterKey); + } +}; + +const TwoFactorSecret = z.object({ + /** + * The 2FA secret code. + */ + secretCode: z.string(), + /** + * A base64 encoded "image/png". + */ + qrCode: z.string(), +}); + +export type TwoFactorSecret = z.infer; + +/** + * Start a TOTP based two factor setup process by fetching a secret code (and + * the corresponding QR code) from remote. + * + * Once the user provides us with a TOTP generated using the provided secret, we + * can finish the setup with {@link setupTwoFactorFinish}. + */ +export const setupTwoFactor = async (): Promise => { const res = await fetch(await apiURL("/users/two-factor/setup"), { method: "POST", headers: await authenticatedRequestHeaders(), @@ -292,17 +846,55 @@ export const setupTwoFactor = async () => { return TwoFactorSecret.parse(await res.json()); }; +/** + * Finish the TOTP based two factor setup by provided a previously obtained + * secret (using {@link setupTwoFactor}) and the current TOTP generated using + * that secret. + * + * This updates both the state both locally and on remote. + * + * @param secretCode The value of {@link secretCode} from the + * {@link TwoFactorSecret} obtained by {@link setupTwoFactor}. + * + * @param totp The current TOTP corresponding to {@link secretCode}. + */ +export const setupTwoFactorFinish = async ( + secretCode: string, + totp: string, +) => { + const box = await encryptBox(secretCode, await getUserRecoveryKey()); + await enableTwoFactor({ + code: totp, + encryptedTwoFactorSecret: box.encryptedData, + twoFactorSecretDecryptionNonce: box.nonce, + }); + await setLSUser({ ...getData("user"), isTwoFactorEnabled: true }); +}; + interface EnableTwoFactorRequest { + /** + * The current value of the TOTP corresponding to the two factor {@link + * secretCode} obtained from a previous call to {@link setupTwoFactor}. + */ code: string; + /** + * The {@link secretCode} encrypted with the user's recovery key. + * + * This is used in the case of second factor recovery. + */ encryptedTwoFactorSecret: string; + /** + * The nonce that was used when encrypting {@link encryptedTwoFactorSecret}. + */ twoFactorSecretDecryptionNonce: string; } /** - * Enable two factor for the user by providing the 2FA code and the encrypted - * secret from a previous call to {@link setupTwoFactor}. + * Enable the TOTP based two factor for the user by providing the current 2FA + * code corresponding the two factor secret, and encrypted secrets for future + * recovery (if needed). */ -export const enableTwoFactor = async (req: EnableTwoFactorRequest) => +const enableTwoFactor = async (req: EnableTwoFactorRequest) => ensureOk( await fetch(await apiURL("/users/two-factor/enable"), { method: "POST", @@ -311,10 +903,160 @@ export const enableTwoFactor = async (req: EnableTwoFactorRequest) => }), ); -export const setRecoveryKey = async (token: string, recoveryKey: RecoveryKey) => - HTTPService.put( - await apiURL("/users/recovery-key"), - recoveryKey, - undefined, - { "X-Auth-Token": token }, +/** + * The result of a successful two factor verification (TOTP or passkey), + * recovery removal (TOTP) or recovery bypass (passkey). + */ +export const TwoFactorAuthorizationResponse = z.object({ + /** + * The user's ID. + */ + id: z.number(), + /** + * The user's key attributes. + */ + keyAttributes: RemoteKeyAttributes, + /** + * A encrypted auth token. + */ + encryptedToken: z.string(), +}); + +export type TwoFactorAuthorizationResponse = z.infer< + typeof TwoFactorAuthorizationResponse +>; + +export const verifyTwoFactor = async ( + code: string, + sessionID: string, +): Promise => { + const res = await fetch(await apiURL("/users/two-factor/verify"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify({ code, sessionID }), + }); + ensureOk(res); + return TwoFactorAuthorizationResponse.parse(await res.json()); +}; + +/** The type of the second factor we're trying to act on */ +export type TwoFactorType = "totp" | "passkey"; + +const TwoFactorRecoveryResponse = z.object({ + /** + * The recovery secret, encrypted using the user's recovery key. + */ + encryptedSecret: z.string(), + /** + * The nonce used during encryption of {@link encryptedSecret}. + */ + secretDecryptionNonce: z.string(), +}); + +export type TwoFactorRecoveryResponse = z.infer< + typeof TwoFactorRecoveryResponse +>; + +/** + * Initiate second factor reset or bypass by requesting the encrypted second + * factor recovery secret (and nonce) from remote. The user can then decrypt + * these using their recovery key to reset or bypass their second factor. + * + * @param twoFactorType The type of second factor to reset or bypass. + * + * @param sessionID A two factor session ID ({@link twoFactorSessionID} or + * {@link passkeySessionID}) for the user. + * + * [Note: Second factor recovery] + * + * 1. When setting up a TOTP based second factor, client sends a (encrypted 2fa + * recovery secret, nonce) pair to remote. This is a randomly generated + * secret (and nonce) encrypted using the user's recovery key. + * + * 2. Similarly, when setting up a passkey as the second factor, the client + * sends a encrypted recovery secret (see {@link configurePasskeyRecovery}). + * + * 3. When the user wishes to reset or bypass their second factor, the client + * asks remote for these encrypted secrets (using {@link recoverTwoFactor}). + * + * 4. User then enters their recovery key, which the client uses to decrypt the + * recovery secret and provide it back to remote for verification (using + * {@link removeTwoFactor}). + * + * 5. If the recovery secret matches, then remote resets (TOTP based) or bypass + * (passkey based) the user's second factor. + */ +export const recoverTwoFactor = async ( + twoFactorType: TwoFactorType, + sessionID: string, +): Promise => { + const res = await fetch( + await apiURL("/users/two-factor/recover", { twoFactorType, sessionID }), + { headers: publicRequestHeaders() }, ); + ensureOk(res); + return TwoFactorRecoveryResponse.parse(await res.json()); +}; + +/** + * Finish the second factor recovery / bypass initiated by + * {@link recoverTwoFactor} using the provided recovery key mnemonic entered by + * the user. + * + * See: [Note: Second factor recovery]. + * + * This completes the recovery process both locally, and on remote. + * + * @param twoFactorType The second factor type (same value as what would've been + * passed to {@link recoverTwoFactor} for obtaining {@link recoveryResponse}). + * + * @param sessionID The second factor session ID (same value as what would've + * been passed to {@link recoverTwoFactor} for obtaining + * {@link recoveryResponse}). + * + * @param recoveryResponse The response to a previous call to + * {@link recoverTwoFactor}. + * + * @param recoveryKeyMnemonic The 24-word BIP-39 recovery key mnemonic provided + * by the user to complete recovery. + */ +export const recoverTwoFactorFinish = async ( + twoFactorType: TwoFactorType, + sessionID: string, + recoveryResponse: TwoFactorRecoveryResponse, + recoveryKeyMnemonic: string, +) => { + const { encryptedSecret: encryptedData, secretDecryptionNonce: nonce } = + recoveryResponse; + const twoFactorSecret = await decryptBox( + { encryptedData, nonce }, + await recoveryKeyFromMnemonic(recoveryKeyMnemonic), + ); + const { id, keyAttributes, encryptedToken } = await removeTwoFactor( + twoFactorType, + sessionID, + twoFactorSecret, + ); + await setLSUser({ + ...getData("user"), + id, + isTwoFactorEnabled: false, + encryptedToken, + token: undefined, + }); + saveKeyAttributes(keyAttributes); +}; + +const removeTwoFactor = async ( + twoFactorType: TwoFactorType, + sessionID: string, + secret: string, +): Promise => { + const res = await fetch(await apiURL("/users/two-factor/remove"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify({ twoFactorType, sessionID, secret }), + }); + ensureOk(res); + return TwoFactorAuthorizationResponse.parse(await res.json()); +}; diff --git a/web/packages/accounts/utils/helpers.ts b/web/packages/accounts/utils/helpers.ts new file mode 100644 index 0000000000..f165453d70 --- /dev/null +++ b/web/packages/accounts/utils/helpers.ts @@ -0,0 +1,35 @@ +// TODO: Audit this file, this can be better. e.g. do we need the Object.assign? + +import { type KeyAttributes } from "ente-accounts/services/user"; +import { boxSealOpenBytes, decryptBox, toB64URLSafe } from "ente-base/crypto"; +import { getData, setLSUser } from "ente-shared/storage/localStorage"; + +export async function decryptAndStoreToken( + keyAttributes: KeyAttributes, + masterKey: string, +) { + const user = getData("user"); + const { encryptedToken } = user; + + if (encryptedToken && encryptedToken.length > 0) { + const { encryptedSecretKey, secretKeyDecryptionNonce, publicKey } = + keyAttributes; + const privateKey = await decryptBox( + { + encryptedData: encryptedSecretKey, + nonce: secretKeyDecryptionNonce, + }, + masterKey, + ); + + const decryptedToken = await toB64URLSafe( + await boxSealOpenBytes(encryptedToken, { publicKey, privateKey }), + ); + + await setLSUser({ + ...user, + token: decryptedToken, + encryptedToken: null, + }); + } +} diff --git a/web/packages/base/app.ts b/web/packages/base/app.ts index 74b891a9c0..e330e41a83 100644 --- a/web/packages/base/app.ts +++ b/web/packages/base/app.ts @@ -62,8 +62,7 @@ export const staticAppTitle = { /** * Client "package names" for our app. * - * The package name is used as the (a) "X-Client-Package" header in API - * requests, and (b) as the identifier in inline user agent strings in payloads. + * The package name is used as the "X-Client-Package" header in API requests. * * In cases where this code works for both a web and a desktop app for the same * app (currently only photos), this will be the platform specific package name. @@ -72,9 +71,7 @@ export const clientPackageName = (() => { if (isDesktop) { if (appName != "photos") throw new Error(`Unsupported desktop appName ${appName}`); - if (!desktopAppVersion) - throw new Error(`desktopAppVersion is not defined`); - return `io.ente.photos.desktop/${desktopAppVersion}`; + return "io.ente.photos.desktop"; } return { accounts: "io.ente.accounts.web", @@ -83,3 +80,23 @@ export const clientPackageName = (() => { photos: "io.ente.photos.web", }[appName]; })(); + +/** + * The client package name, augmented with the client version where applicable. + * + * This string is used as the "client identifier" in payloads (e.g. ML data + * generated by a client). + * + * For the desktop app, it will be client package name and version. Otherwise, + * it'll just be the same as {@link clientPackageName}. + */ +export const clientIdentifier = (() => { + if (isDesktop) { + if (appName != "photos") + throw new Error(`Unsupported desktop appName ${appName}`); + if (!desktopAppVersion) + throw new Error(`desktopAppVersion is not defined`); + return `io.ente.photos.desktop/${desktopAppVersion}`; + } + return clientPackageName; +})(); diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index daadf700e8..6d1ad60ae9 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -71,7 +71,7 @@ export const OverflowMenu: React.FC< setAnchorEl(undefined)} slotProps={{ @@ -118,11 +118,16 @@ interface OverflowMenuOptionProps { export const OverflowMenuOption: React.FC< React.PropsWithChildren > = ({ onClick, color = "primary", startIcon, endIcon, children }) => { - const menuContext = useContext(OverflowMenuContext)!; + const menuContext = useContext(OverflowMenuContext); const handleClick = () => { onClick(); - menuContext.close(); + // We might've already been closed as a result of our containing menu + // getting closed. An example of this is the "Sort by" option in the + // album options overflow menu, where the `onClick` above will result in + // `onClose` being called on our parent menu, so `menuContext` will be + // undefined when we get here. + menuContext?.close(); }; return ( diff --git a/web/packages/base/components/RowButton.tsx b/web/packages/base/components/RowButton.tsx index cfe328f7ee..ef72d5af01 100644 --- a/web/packages/base/components/RowButton.tsx +++ b/web/packages/base/components/RowButton.tsx @@ -10,6 +10,7 @@ import { import { EnteSwitch } from "ente-base/components/EnteSwitch"; import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton"; import React from "react"; +import { ActivityIndicator } from "./mui/ActivityIndicator"; interface RowButtonGroupTitleProps { /** @@ -411,3 +412,11 @@ export const RowLabel: React.FC = ({ startIcon, label }) => ( ); + +/** + * A variant of {@link ActivityIndicator} with defaults suitable to be used as the + * {@link EndIcon} of a {@link RowButton}. + */ +export const RowButtonEndActivityIndicator: React.FC = () => ( + +); diff --git a/web/packages/base/components/SingleInputDialog.tsx b/web/packages/base/components/SingleInputDialog.tsx index 11f23575ad..9a1cf64b69 100644 --- a/web/packages/base/components/SingleInputDialog.tsx +++ b/web/packages/base/components/SingleInputDialog.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogContent, DialogTitle } from "@mui/material"; import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; -import React from "react"; +import React, { useCallback } from "react"; import { SingleInputForm, type SingleInputFormProps } from "./SingleInputForm"; type SingleInputDialogProps = ModalVisibilityProps & @@ -25,10 +25,13 @@ export const SingleInputDialog: React.FC = ({ title, ...rest }) => { - const handleSubmit = async (value: string) => { - await onSubmit(value); - onClose(); - }; + const handleSubmit: SingleInputFormProps["onSubmit"] = useCallback( + async (value, setFieldError) => { + await onSubmit(value, setFieldError); + onClose(); + }, + [onClose, onSubmit], + ); return ( & { + /** + * The type attribute of the HTML input element that will be used. + * + * Default is "text". + * + * In addition to changing the behaviour of the HTML input element, the + * {@link SingleInputForm} component also has special casing for type + * "password", wherein it'll show an adornment at the end of the text field + * allowing the user to show or hide the password. + */ + inputType?: TextFieldProps["type"]; /** * The initial value, if any, to prefill in the input. */ initialValue?: string; + /** + * Color of the submit button. + * + * Default: "accent". + */ + submitButtonColor?: ButtonProps["color"]; /** * Title for the submit button. */ @@ -23,8 +46,11 @@ export type SingleInputFormProps = Pick< * * This function is called when the user activates the cancel button in the * form. + * + * If this is not provided, then only a full width submit button will be + * shown in the form (below the input). */ - onCancel: () => void; + onCancel?: () => void; /** * Submission handler. A callback invoked when the submit button is pressed. * @@ -34,13 +60,29 @@ export type SingleInputFormProps = Pick< * If this function rejects then a generic error helper text is shown below * the text input, and the input (/ buttons) reenabled. * + * This function is also passed an function that can be used to explicitly + * set the error message that as shown below the text input when a specific + * problem occurs during submission. + * * @param name The current value of the text input. + * + * @param setFieldError A function that can be called to set the error message + * shown below the text input if submission fails. + * + * Note that if {@link setFieldError} is called, then the {@link onSubmit} + * function should not throw, otherwise the error message shown by + * {@link setFieldError} will get overwritten by the generic error message. */ - onSubmit: ((name: string) => void) | ((name: string) => Promise); + onSubmit: + | ((name: string, setFieldError: (message: string) => void) => void) + | (( + name: string, + setFieldError: (message: string) => void, + ) => Promise); }; /** - * A TextField and two buttons. + * A TextField and cancel/submit buttons. * * A common requirement is taking a single textual input from the user. This is * a form suitable for that purpose. It contains a single MUI {@link TextField} @@ -49,32 +91,59 @@ export type SingleInputFormProps = Pick< * Submission is handled as an async function, during which the input is * disabled and a loading indicator is shown. Errors during submission are shown * as the helper text associated with the text field. + * + * The input field in the form takes autoFocus automatically on mount. Turn off + * the {@link autoFocus} to disable this behaviour if needed. */ export const SingleInputForm: React.FC = ({ + inputType, initialValue, + autoFocus, submitButtonTitle, + submitButtonColor, onCancel, onSubmit, ...rest }) => { + const [showPassword, setShowPassword] = useState(false); + + const handleToggleShowHidePassword = useCallback( + () => setShowPassword((show) => !show), + [], + ); + const formik = useFormik({ initialValues: { value: initialValue ?? "" }, onSubmit: async (values, { setFieldError }) => { const value = values.value; + const setValueFieldError = (message: string) => + setFieldError("value", message); + if (!value) { - setFieldError("value", t("required")); + setValueFieldError(t("required")); return; } try { - await onSubmit(value); + await onSubmit(value, setValueFieldError); } catch (e) { log.error(`Failed to submit input ${value}`, e); - setFieldError("value", t("generic_error")); + setValueFieldError(t("generic_error")); } }, }); - // Note: [Use space as default TextField helperText] + const submitButton = ( + + {submitButtonTitle} + + ); + + // [Note: Use space as default TextField helperText] // // For MUI text fields that use a conditional helperText, e.g. in case of // errors, use an space as the default helperText in the other cases to @@ -86,31 +155,44 @@ export const SingleInputForm: React.FC = ({ name="value" value={formik.values.value} onChange={formik.handleChange} - type="text" + type={showPassword ? "text" : (inputType ?? "text")} fullWidth + autoFocus={autoFocus ?? true} margin="normal" disabled={formik.isSubmitting} error={!!formik.errors.value} helperText={formik.errors.value ?? " "} + slotProps={{ + input: + inputType == "password" + ? { + endAdornment: ( + + ), + } + : {}, + }} {...rest} /> - - - {t("cancel")} - - - {submitButtonTitle} - - + {onCancel ? ( + + + {t("cancel")} + + {submitButton} + + ) : ( + submitButton + )} ); }; diff --git a/web/packages/base/components/Titlebar.tsx b/web/packages/base/components/Titlebar.tsx deleted file mode 100644 index 9a1f7cc6ed..0000000000 --- a/web/packages/base/components/Titlebar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import CloseIcon from "@mui/icons-material/Close"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; -import { FlexWrapper } from "ente-shared/components/Container"; -import React from "react"; - -interface TitlebarProps { - title: string; - caption?: string; - onClose: () => void; - backIsClose?: boolean; - onRootClose?: () => void; - actionButton?: React.JSX.Element; -} - -// TODO: Deprecated in favor of SidebarDrawerTitlebarProps where possible (will -// revisit the remaining use cases once those have migrated). -export const Titlebar: React.FC = ({ - title, - caption, - onClose, - backIsClose, - actionButton, - onRootClose, -}) => { - return ( - <> - - - {backIsClose ? : } - - - {actionButton && actionButton} - {!backIsClose && ( - - - - )} - - - - {title} - - {caption} - - - - ); -}; diff --git a/web/packages/base/components/mui/PasswordInputAdornment.tsx b/web/packages/base/components/mui/PasswordInputAdornment.tsx new file mode 100644 index 0000000000..08c4771bc6 --- /dev/null +++ b/web/packages/base/components/mui/PasswordInputAdornment.tsx @@ -0,0 +1,49 @@ +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import { IconButton, InputAdornment } from "@mui/material"; +import { t } from "i18next"; +import React from "react"; + +interface ShowHidePasswordInputAdornmentProps { + /** + * When `true`, the password is being shown. + */ + showPassword: boolean; + /** + * Called when the user wants to toggle the state of {@link showPassword}. + */ + onToggle: () => void; +} + +/** + * A MUI {@link InputAdornment} that can be used at the trailing edge of a + * password input field to allow the user to toggle the visibility of the + * password. + */ +export const ShowHidePasswordInputAdornment: React.FC< + ShowHidePasswordInputAdornmentProps +> = ({ showPassword, onToggle: onToggle }) => { + // Prevent password field from losing focus when the input adornment is + // clicked by ignoring both the mouse up and down events. This is the + // approach mentioned in the MUI docs. + // https://mui.com/material-ui/react-text-field/#input-adornments + const preventDefault = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + {showPassword ? : } + + + ); +}; diff --git a/web/packages/base/components/mui/SidebarDrawer.tsx b/web/packages/base/components/mui/SidebarDrawer.tsx index 73b86d9869..93862059e6 100644 --- a/web/packages/base/components/mui/SidebarDrawer.tsx +++ b/web/packages/base/components/mui/SidebarDrawer.tsx @@ -164,18 +164,43 @@ export const SidebarDrawerTitlebar: React.FC = ({ - + {title} {caption} - + ); + +/** + * A variant of {@link NestedSidebarDrawer} that additionally shows a title. + * + * {@link NestedSidebarDrawer} is for second level, nested drawers that are + * shown atop an already visible {@link SidebarDrawer}. This component combines + * the {@link NestedSidebarDrawer} with a {@link SidebarDrawerTitlebar} and some + * standard spacing, so that the caller can just provide the content as the + * children. + */ +export const TitledNestedSidebarDrawer: React.FC< + React.PropsWithChildren< + NestedSidebarDrawerVisibilityProps & + Pick & + SidebarDrawerTitlebarProps + > +> = ({ open, onClose, onRootClose, anchor, children, ...rest }) => ( + + + + {children} + + +); diff --git a/web/packages/base/components/utils/mui-theme.d.ts b/web/packages/base/components/utils/mui-theme.d.ts index faeef7901c..1cdcdef9e2 100644 --- a/web/packages/base/components/utils/mui-theme.d.ts +++ b/web/packages/base/components/utils/mui-theme.d.ts @@ -186,7 +186,6 @@ declare module "@mui/material/Button" { success: false; info: false; warning: false; - inherit: false; // Add our custom palette colors. accent: true; critical: true; @@ -200,7 +199,6 @@ declare module "@mui/material/IconButton" { success: false; info: false; warning: false; - inherit: false; } } diff --git a/web/packages/base/components/utils/theme.ts b/web/packages/base/components/utils/theme.ts index 44c223c7ba..6962c90617 100644 --- a/web/packages/base/components/utils/theme.ts +++ b/web/packages/base/components/utils/theme.ts @@ -1,7 +1,6 @@ -import type { Theme } from "@mui/material"; +import type { Theme, TypographyVariantsOptions } from "@mui/material"; import { createTheme } from "@mui/material"; -import type { Components } from "@mui/material/styles/components"; -import type { TypographyOptions } from "@mui/material/styles/createTypography"; +import type { Components } from "@mui/material/styles"; import type { AppName } from "ente-base/app"; const getTheme = (appName: AppName): Theme => { @@ -411,7 +410,7 @@ const getColorSchemes = (colors: ReturnType) => ({ * to bother with the light variant (though for consistency of specifying every * value, we alias it the same weight as regular, 500). */ -const typography: TypographyOptions = { +const typography: TypographyVariantsOptions = { fontFamily: '"Inter Variable", sans-serif', fontWeightLight: 500, fontWeightRegular: 500 /* CSS baseline reset sets this as the default */, @@ -552,11 +551,26 @@ const components: Components = { MuiPaper: { styleOverrides: { root: { - // MUI applies a semi-transparent background image for elevation - // in dark mode. Remove it to match background for our designs. - backgroundImage: "none", - // Use our paper shadow. - boxShadow: "var(--mui-palette-boxShadow-paper)", + variants: [ + { + // Use our "paper" shadow for elevated Paper. + props: { variant: "elevation" }, + style: { + // MUI applies a semi-transparent background image + // for elevation in dark mode. Remove it to match + // background for our designs. + backgroundImage: "none", + // Use our paper shadow. + boxShadow: "var(--mui-palette-boxShadow-paper)", + }, + }, + { + // Undo the effects of variant "elevation" case above + // case when elevation is 0. + props: { elevation: 0 }, + style: { boxShadow: "none" }, + }, + ], }, }, }, @@ -682,11 +696,8 @@ const components: Components = { props: { color: "secondary" }, style: { color: "var(--mui-palette-stroke-muted)" }, }, - { - props: { color: "disabled" }, - style: { color: "var(--mui-palette-stroke-faint)" }, - }, ], + "&.Mui-disabled": { color: "var(--mui-palette-stroke-faint)" }, }, }, }, @@ -710,6 +721,13 @@ const components: Components = { }, }, }, + + MuiAlert: { + defaultProps: { + // Use the outlined variant by default (instead of "standard"). + variant: "outlined", + }, + }, }; // Exports --- diff --git a/web/packages/base/crypto/ente-impl.ts b/web/packages/base/crypto/ente-impl.ts deleted file mode 100644 index 27c8a3a688..0000000000 --- a/web/packages/base/crypto/ente-impl.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** Careful when adding add other imports! */ -import * as libsodium from "./libsodium"; -import type { BytesOrB64, EncryptedBlob, EncryptedFile } from "./types"; - -export const _toB64 = libsodium.toB64; - -export const _toB64URLSafe = libsodium.toB64URLSafe; - -export const _fromB64 = libsodium.fromB64; - -export const _toHex = libsodium.toHex; - -export const _fromHex = libsodium.fromHex; - -export const _generateBoxKey = libsodium.generateBoxKey; - -export const _generateBlobOrStreamKey = libsodium.generateBlobOrStreamKey; - -export const _encryptBoxB64 = libsodium.encryptBoxB64; - -export const _encryptBlob = libsodium.encryptBlob; - -export const _encryptBlobB64 = libsodium.encryptBlobB64; - -export const _encryptThumbnail = async ( - data: BytesOrB64, - key: BytesOrB64, -): Promise => { - const { encryptedData, decryptionHeader } = await _encryptBlob(data, key); - return { - encryptedData, - decryptionHeader: await libsodium.toB64(decryptionHeader), - }; -}; - -export const _encryptStreamBytes = libsodium.encryptStreamBytes; - -export const _initChunkEncryption = libsodium.initChunkEncryption; - -export const _encryptStreamChunk = libsodium.encryptStreamChunk; - -export const _encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) => - _encryptBlobB64(new TextEncoder().encode(JSON.stringify(jsonValue)), key); - -// Deprecated, translates to the old API for now. -export const _encryptMetadataJSON = async (r: { - jsonValue: unknown; - keyB64: string; -}) => { - const { encryptedData, decryptionHeader } = await _encryptMetadataJSON_New( - r.jsonValue, - r.keyB64, - ); - return { - encryptedDataB64: encryptedData, - decryptionHeaderB64: decryptionHeader, - }; -}; - -export const _decryptBox = libsodium.decryptBox; - -export const _decryptBoxB64 = libsodium.decryptBoxB64; - -export const _decryptBlob = libsodium.decryptBlob; - -export const _decryptBlobB64 = libsodium.decryptBlobB64; - -export const _decryptThumbnail = _decryptBlob; - -export const _decryptStreamBytes = libsodium.decryptStreamBytes; - -export const _initChunkDecryption = libsodium.initChunkDecryption; - -export const _decryptStreamChunk = libsodium.decryptStreamChunk; - -export const _decryptMetadataJSON_New = async ( - blob: EncryptedBlob, - key: BytesOrB64, -) => - JSON.parse( - new TextDecoder().decode(await _decryptBlob(blob, key)), - ) as unknown; - -export const _decryptMetadataJSON = async (r: { - encryptedDataB64: string; - decryptionHeaderB64: string; - keyB64: string; -}) => - _decryptMetadataJSON_New( - { - encryptedData: r.encryptedDataB64, - decryptionHeader: r.decryptionHeaderB64, - }, - r.keyB64, - ); - -export const _generateKeyPair = libsodium.generateKeyPair; - -export const _boxSeal = libsodium.boxSeal; - -export const _boxSealOpen = libsodium.boxSealOpen; - -export const _deriveKey = libsodium.deriveKey; - -export const _deriveSensitiveKey = libsodium.deriveSensitiveKey; - -export const _deriveInteractiveKey = libsodium.deriveInteractiveKey; diff --git a/web/packages/base/crypto/index.ts b/web/packages/base/crypto/index.ts index 214f4d119e..8ffa6dfbb5 100644 --- a/web/packages/base/crypto/index.ts +++ b/web/packages/base/crypto/index.ts @@ -9,54 +9,97 @@ * [Note: Crypto code hierarchy] * * 1. ente-base/crypto (Crypto API for our code) - * 2. ente-base/crypto/libsodium (Lower level wrappers over libsodium) + * 2. ente-base/crypto/libsodium (The actual implementation) * 3. libsodium-wrappers (JavaScript bindings to libsodium) * * Our cryptography primitives are provided by libsodium, specifically, its - * JavaScript bindings ("libsodium-wrappers"). This is the lowest layer. Note - * that we use the sumo variant, "libsodium-wrappers-sumo", since the standard - * variant does not provide the `crypto_pwhash_*` functions. + * JavaScript bindings ("libsodium-wrappers"). That is the lowest layer. + * + * > Note that we use the sumo variant, "libsodium-wrappers-sumo", since the + * standard variant does not provide the `crypto_pwhash_*` functions. * * Direct usage of "libsodium-wrappers" is restricted to `crypto/libsodium.ts`. - * This is the next higher layer. Usually the functions in this file are thin + * That is the next higher layer. Usually the functions in this file are thin * wrappers over the raw libsodium APIs, with a bit of massaging. They also * ensure that sodium.ready has been called before accessing libsodium's APIs, * thus all the functions it exposes are async. * - * The highest layer is this file, `crypto/index.ts`. These are usually direct - * proxies (or simple compositions) of functionality exposed by - * `crypto/libsodium.ts`, but they automatically defer to a worker thread. + * Direct usage of "libsodium-wrappers" is restricted to this file, + * `crypto/index.ts`. This is the highest layer. These are direct proxies to + * functions exposed by `crypto/libsodium.ts`, but they automatically defer to a + * worker thread if we're not already running on one. More on this below. + * + * --- + * + * [Note: Using libsodium in worker thread] + * + * This file, `crypto/index.ts`, and `crypto/worker.ts` are mostly logic-less + * trampolines meant to allow us to seamlessly use the the same API both from + * the main thread or from a web worker whilst ensuring that the implementation + * never runs on the main thread. * * Cryptographic operations like encryption are CPU intensive and would cause * the UI to stutter if used directly on the main thread. To keep the UI smooth, * we instead want to run them in a web worker. However, sometimes we already * _are_ running in a web worker, and delegating to another worker is wasteful. * - * To handle both these scenario, the implementation of the functions in this - * file are split into the external API, and the underlying implementation - * (denoted by an "_" prefix). To avoid a circular dependency during webpack - * imports, we need to keep the implementation functions in a separate file - * (`ente-impl.ts`). + * The external API functions provided by this file check to see if we're + * already in a web worker, and if so directly invoke the implementation. + * Otherwise the call the sibling function in a shared "crypto" web worker + * (which then invokes the implementation function, but this time in the context + * of a web worker). * - * The external API functions check to see if we're already in a web worker, and - * if so directly invoke the implementation. Otherwise the call the sibling - * function in a shared "crypto" web worker (which then invokes the - * implementation function, but this time in the context of a web worker). + * As a consumer, it is safe to just call functions in this file, and they'll + * just do the right thing based on the context. However, it is also fine to + * explicitly get an handle to a crypto web worker and use that. e.g., the + * uploader creates it own crypto worker instances and directly calls the + * functions in the workers that it created instead of going through this file. * - * Also, some code (e.g. the uploader) creates it own crypto worker instances, - * and thus directly calls the functions in the web worker that it created - * instead of going through this file. + * --- + * + * [Note: Crypto layer API data types] + * + * There are two primary types used when exchanging data with these functions: + * + * 1. Base64 strings. Unless stated otherwise, all strings are taken as base64 + * encoded representations of the underlying data. Usually, the unqualified + * function deals with base64 strings, since they also are the data type in + * which we usually store and send the data. + * + * 2. Raw bytes. Uint8Arrays are byte arrays. The functions that deal with bytes + * are usually indicated by a *Bytes suffix in their name, but not always + * since it might also be the natural choice for functions that deal with + * larger amounts of data. + * + * Where relevant and useful, functions also accept a union of these two - a + * {@link BytesOrB64} where the implementation will automatically convert + * to/from base64 to bytes if needed, thus saving on unnecessary conversions at + * the caller side. + * + * Apart from these two, there are other secondary, one off types. + * + * 1. Hex representations of the bytes. These are indicated by the *Hex suffix + * on the functions dealing with them. + * + * 2. JSON values. These are indicated by the *JSON suffix on the functions + * dealing with them. */ import { ComlinkWorker } from "ente-base/worker/comlink-worker"; -import { type StateAddress } from "libsodium-wrappers-sumo"; -import { assertionFailed } from "../assert"; import { inWorker } from "../env"; -import * as ei from "./ente-impl"; +import * as libsodium from "./libsodium"; import type { BytesOrB64, + DerivedKey, EncryptedBlob, + EncryptedBlobB64, + EncryptedBlobBytes, EncryptedBox, + EncryptedBoxB64, EncryptedFile, + InitChunkDecryptionResult, + InitChunkEncryptionResult, + KeyPair, + SodiumStateAddress, } from "./types"; import type { CryptoWorker } from "./worker"; @@ -68,7 +111,7 @@ let _comlinkWorker: ComlinkWorker | undefined; /** * Lazily created, cached, instance of a CryptoWorker web worker. */ -export const sharedCryptoWorker = async () => +export const sharedCryptoWorker = () => (_comlinkWorker ??= createComlinkCryptoWorker()).remote; /** A shorter alias of {@link sharedCryptoWorker} for use within this file. */ @@ -84,74 +127,83 @@ export const createComlinkCryptoWorker = () => new Worker(new URL("worker.ts", import.meta.url)), ); -/** - * Some of the potentially CPU intensive functions below have not yet been - * needed on the main thread, and for these we don't have a corresponding - * sharedCryptoWorker method. - * - * This assertion will let us know when we need to implement them. This will - * gracefully degrade in production: the functionality will work, just that the - * crypto operations will happen on the main thread itself. - */ -const assertInWorker = (x: T): T => { - if (!inWorker()) assertionFailed("Currently only usable in a web worker"); - return x; -}; - /** * Convert bytes ({@link Uint8Array}) to a base64 string. */ -export const toB64 = (bytes: Uint8Array) => - inWorker() ? ei._toB64(bytes) : sharedWorker().then((w) => w.toB64(bytes)); - -/** - * URL safe variant of {@link toB64}. - */ -export const toB64URLSafe = (bytes: Uint8Array) => +export const toB64 = (bytes: Uint8Array): Promise => inWorker() - ? ei._toB64URLSafe(bytes) - : sharedWorker().then((w) => w.toB64URLSafe(bytes)); + ? libsodium.toB64(bytes) + : sharedWorker().then((w) => w.toB64(bytes)); /** * Convert a base64 string to bytes ({@link Uint8Array}). */ -export const fromB64 = (b64String: string) => +export const fromB64 = (b64String: string): Promise => inWorker() - ? ei._fromB64(b64String) + ? libsodium.fromB64(b64String) : sharedWorker().then((w) => w.fromB64(b64String)); +/** + * URL safe variant of {@link toB64}. + */ +export const toB64URLSafe = (bytes: Uint8Array): Promise => + inWorker() + ? libsodium.toB64URLSafe(bytes) + : sharedWorker().then((w) => w.toB64URLSafe(bytes)); + +/** + * URL safe variant of {@link toB64} that does not add any padding ("=" + * characters). + */ +export const toB64URLSafeNoPadding = (bytes: Uint8Array): Promise => + inWorker() + ? libsodium.toB64URLSafeNoPadding(bytes) + : sharedWorker().then((w) => w.toB64URLSafeNoPadding(bytes)); + +/** + * URL safe unpadded variant of {@link fromB64}. + */ +export const fromB64URLSafeNoPadding = ( + b64String: string, +): Promise => + inWorker() + ? libsodium.fromB64URLSafeNoPadding(b64String) + : sharedWorker().then((w) => w.fromB64URLSafeNoPadding(b64String)); + /** * Convert a base64 string to the hex representation of the underlying bytes. */ -export const toHex = (b64String: string) => +export const toHex = (b64String: string): Promise => inWorker() - ? ei._toHex(b64String) + ? libsodium.toHex(b64String) : sharedWorker().then((w) => w.toHex(b64String)); /** * Convert a hex string to the base64 representation of the underlying bytes. */ -export const fromHex = (hexString: string) => +export const fromHex = (hexString: string): Promise => inWorker() - ? ei._fromHex(hexString) + ? libsodium.fromHex(hexString) : sharedWorker().then((w) => w.fromHex(hexString)); /** - * Return a new randomly generated 256-bit key (as a base64 string) suitable for - * use with the *Box encryption functions. + * Return a new randomly generated 256-bit key (as a base64 string). + * + * The returned key is suitable for use with the *Box encryption functions, and + * as a general encryption key (e.g. as the user's master key or recovery key). */ -export const generateBoxKey = () => +export const generateKey = (): Promise => inWorker() - ? ei._generateBoxKey() - : sharedWorker().then((w) => w.generateBoxKey()); + ? libsodium.generateKey() + : sharedWorker().then((w) => w.generateKey()); /** * Return a new randomly generated 256-bit key (as a base64 string) suitable for * use with the *Blob or *Stream encryption functions. */ -export const generateBlobOrStreamKey = () => +export const generateBlobOrStreamKey = (): Promise => inWorker() - ? ei._generateBlobOrStreamKey() + ? libsodium.generateBlobOrStreamKey() : sharedWorker().then((w) => w.generateBlobOrStreamKey()); /** @@ -160,94 +212,62 @@ export const generateBlobOrStreamKey = () => * * Both the encrypted data and the nonce are returned as base64 strings. * - * Use {@link decryptBoxB64} to decrypt the result. + * Use {@link decryptBox} to decrypt the result. * * > The suffix "Box" comes from the fact that it uses the so called secretbox * > APIs provided by libsodium under the hood. * > * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] */ -export const encryptBoxB64 = (data: BytesOrB64, key: BytesOrB64) => +export const encryptBox = ( + data: BytesOrB64, + key: BytesOrB64, +): Promise => inWorker() - ? ei._encryptBoxB64(data, key) - : sharedWorker().then((w) => w.encryptBoxB64(data, key)); + ? libsodium.encryptBox(data, key) + : sharedWorker().then((w) => w.encryptBox(data, key)); /** * Encrypt the given data, returning a blob containing the encrypted data and a - * decryption header. + * decryption header as base64 strings. * * This function is usually used to encrypt data associated with an Ente object * (file, collection, entity) using the object's key. * - * Use {@link decryptBlob} to decrypt the result. + * Use {@link decryptBlob} or {@link decryptBlobBytes} to decrypt the result. * * > The suffix "Blob" comes from our convention of naming functions that use * > the secretstream APIs without breaking the data into chunks. * > * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] */ -export const encryptBlob = (data: BytesOrB64, key: BytesOrB64) => - assertInWorker(ei._encryptBlob(data, key)); - -/** - * A variant of {@link encryptBlob} that returns the result components as base64 - * strings. - */ -export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) => +export const encryptBlob = ( + data: BytesOrB64, + key: BytesOrB64, +): Promise => inWorker() - ? ei._encryptBlobB64(data, key) - : sharedWorker().then((w) => w.encryptBlobB64(data, key)); + ? libsodium.encryptBlob(data, key) + : sharedWorker().then((w) => w.encryptBlob(data, key)); /** - * Encrypt the thumbnail for a file. + * A variant of {@link encryptBlob} that returns the result components as bytes + * instead of as base64 strings. * - * This is midway variant of {@link encryptBlob} and {@link encryptBlobB64} that - * returns the decryption header as a base64 string, but leaves the data - * unchanged. - * - * Use {@link decryptThumbnail} to decrypt the result. + * Use {@link decryptBlob} or {@link decryptBlobBytes} to decrypt the result. */ -export const encryptThumbnail = (data: BytesOrB64, key: BytesOrB64) => +export const encryptBlobBytes = ( + data: BytesOrB64, + key: BytesOrB64, +): Promise => inWorker() - ? ei._encryptThumbnail(data, key) - : sharedWorker().then((w) => w.encryptThumbnail(data, key)); - -/** - * Encrypt the given data using chunked streaming encryption, but process all - * the chunks in one go. - */ -export const encryptStreamBytes = async (data: Uint8Array, key: BytesOrB64) => - inWorker() - ? ei._encryptStreamBytes(data, key) - : sharedWorker().then((w) => w.encryptStreamBytes(data, key)); - -/** - * Prepare for chunked streaming encryption using {@link encryptStreamChunk}. - */ -export const initChunkEncryption = async (key: BytesOrB64) => - inWorker() - ? ei._initChunkEncryption(key) - : sharedWorker().then((w) => w.initChunkEncryption(key)); - -/** - * Encrypt a chunk as part of a chunked streaming encryption. - */ -export const encryptStreamChunk = async ( - data: Uint8Array, - state: StateAddress, - isFinalChunk: boolean, -) => - inWorker() - ? ei._encryptStreamChunk(data, state, isFinalChunk) - : sharedWorker().then((w) => - w.encryptStreamChunk(data, state, isFinalChunk), - ); + ? libsodium.encryptBlobBytes(data, key) + : sharedWorker().then((w) => w.encryptBlobBytes(data, key)); /** * Encrypt the JSON metadata associated with an Ente object (file, collection or * entity) using the object's key. * - * This is a variant of {@link encryptBlobB64} tailored for encrypting any + * This is a variant of {@link encryptBlob} tailored for encrypting any * arbitrary metadata associated with an Ente object. For example, it is used * for encrypting the various metadata fields associated with a file, using that * file's key. @@ -263,82 +283,119 @@ export const encryptStreamChunk = async ( * * @param key The encryption key. */ -export const encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) => +export const encryptMetadataJSON = ( + jsonValue: unknown, + key: BytesOrB64, +): Promise => inWorker() - ? ei._encryptMetadataJSON_New(jsonValue, key) - : sharedWorker().then((w) => w.encryptMetadataJSON_New(jsonValue, key)); + ? libsodium.encryptMetadataJSON(jsonValue, key) + : sharedWorker().then((w) => w.encryptMetadataJSON(jsonValue, key)); /** - * Deprecated, use {@link encryptMetadataJSON_New} instead. + * Encrypt the given data using chunked streaming encryption, but process all + * the chunks in one go. */ -export const encryptMetadataJSON = async (r: { - jsonValue: unknown; - keyB64: string; -}) => +export const encryptStreamBytes = ( + data: Uint8Array, + key: BytesOrB64, +): Promise => inWorker() - ? ei._encryptMetadataJSON(r) - : sharedWorker().then((w) => w.encryptMetadataJSON(r)); + ? libsodium.encryptStreamBytes(data, key) + : sharedWorker().then((w) => w.encryptStreamBytes(data, key)); /** - * Decrypt a box encrypted using {@link encryptBoxB64} and returns the decrypted - * bytes. + * Prepare for chunked streaming encryption using {@link encryptStreamChunk}. */ -export const decryptBox = (box: EncryptedBox, key: BytesOrB64) => +export const initChunkEncryption = ( + key: BytesOrB64, +): Promise => inWorker() - ? ei._decryptBox(box, key) + ? libsodium.initChunkEncryption(key) + : sharedWorker().then((w) => w.initChunkEncryption(key)); + +/** + * Encrypt a chunk as part of a chunked streaming encryption. + */ +export const encryptStreamChunk = ( + data: Uint8Array, + state: SodiumStateAddress, + isFinalChunk: boolean, +): Promise => + inWorker() + ? libsodium.encryptStreamChunk(data, state, isFinalChunk) + : sharedWorker().then((w) => + w.encryptStreamChunk(data, state, isFinalChunk), + ); + +/** + * Decrypt a box encrypted using {@link encryptBox} and returns the decrypted + * bytes as a base64 string. + */ +export const decryptBox = ( + box: EncryptedBox, + key: BytesOrB64, +): Promise => + inWorker() + ? libsodium.decryptBox(box, key) : sharedWorker().then((w) => w.decryptBox(box, key)); /** - * Variant of {@link decryptBox} that returns the result as a base64 string. + * Variant of {@link decryptBox} that returns the decrypted bytes as it is + * (without encoding them to base64). */ -export const decryptBoxB64 = (box: EncryptedBox, key: BytesOrB64) => +export const decryptBoxBytes = ( + box: EncryptedBox, + key: BytesOrB64, +): Promise => inWorker() - ? ei._decryptBoxB64(box, key) - : sharedWorker().then((w) => w.decryptBoxB64(box, key)); + ? libsodium.decryptBoxBytes(box, key) + : sharedWorker().then((w) => w.decryptBoxBytes(box, key)); /** - * Decrypt a blob encrypted using either {@link encryptBlob} or - * {@link encryptBlobB64}. + * Decrypt a blob encrypted using either {@link encryptBlobBytes} or + * {@link encryptBlob} and return it as a base64 encoded string. */ -export const decryptBlob = (blob: EncryptedBlob, key: BytesOrB64) => +export const decryptBlob = ( + blob: EncryptedBlob, + key: BytesOrB64, +): Promise => inWorker() - ? ei._decryptBlob(blob, key) + ? libsodium.decryptBlob(blob, key) : sharedWorker().then((w) => w.decryptBlob(blob, key)); /** - * A variant of {@link decryptBlob} that returns the result as a base64 string. + * A variant of {@link decryptBlobBytes} that returns the result bytes directly + * (instead of encoding them as a base64 string). */ -export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) => +export const decryptBlobBytes = ( + blob: EncryptedBlob, + key: BytesOrB64, +): Promise => inWorker() - ? ei._decryptBlobB64(blob, key) - : sharedWorker().then((w) => w.decryptBlobB64(blob, key)); - -/** - * Decrypt the thumbnail encrypted using {@link encryptThumbnail}. - */ -export const decryptThumbnail = (blob: EncryptedBlob, key: BytesOrB64) => - inWorker() - ? ei._decryptThumbnail(blob, key) - : sharedWorker().then((w) => w.decryptThumbnail(blob, key)); + ? libsodium.decryptBlobBytes(blob, key) + : sharedWorker().then((w) => w.decryptBlobBytes(blob, key)); /** * Decrypt the result of {@link encryptStreamBytes}. */ -export const decryptStreamBytes = async ( +export const decryptStreamBytes = ( file: EncryptedFile, key: BytesOrB64, -) => +): Promise => inWorker() - ? ei._decryptStreamBytes(file, key) + ? libsodium.decryptStreamBytes(file, key) : sharedWorker().then((w) => w.decryptStreamBytes(file, key)); /** * Prepare to decrypt the encrypted result produced using {@link initChunkEncryption} and * {@link encryptStreamChunk}. */ -export const initChunkDecryption = async (header: string, key: BytesOrB64) => +export const initChunkDecryption = ( + header: string, + key: BytesOrB64, +): Promise => inWorker() - ? ei._initChunkDecryption(header, key) + ? libsodium.initChunkDecryption(header, key) : sharedWorker().then((w) => w.initChunkDecryption(header, key)); /** @@ -346,12 +403,12 @@ export const initChunkDecryption = async (header: string, key: BytesOrB64) => * * This function is used in tandem with {@link initChunkDecryption}. */ -export const decryptStreamChunk = async ( +export const decryptStreamChunk = ( data: Uint8Array, - state: StateAddress, -) => + state: SodiumStateAddress, +): Promise => inWorker() - ? ei._decryptStreamChunk(data, state) + ? libsodium.decryptStreamChunk(data, state) : sharedWorker().then((w) => w.decryptStreamChunk(data, state)); /** @@ -360,67 +417,77 @@ export const decryptStreamChunk = async ( * @returns The decrypted JSON value. Since TypeScript does not have a native * JSON type, we need to return it as an `unknown`. */ -export const decryptMetadataJSON_New = ( +export const decryptMetadataJSON = ( blob: EncryptedBlob, key: BytesOrB64, -) => +): Promise => inWorker() - ? ei._decryptMetadataJSON_New(blob, key) - : sharedWorker().then((w) => w.decryptMetadataJSON_New(blob, key)); - -/** - * Deprecated, retains the old API. - */ -export const decryptMetadataJSON = (r: { - encryptedDataB64: string; - decryptionHeaderB64: string; - keyB64: string; -}) => - inWorker() - ? ei._decryptMetadataJSON(r) - : sharedWorker().then((w) => w.decryptMetadataJSON(r)); + ? libsodium.decryptMetadataJSON(blob, key) + : sharedWorker().then((w) => w.decryptMetadataJSON(blob, key)); /** * Generate a new public/private keypair. */ -export const generateKeyPair = async () => +export const generateKeyPair = (): Promise => inWorker() - ? ei._generateKeyPair() + ? libsodium.generateKeyPair() : sharedWorker().then((w) => w.generateKeyPair()); /** * Public key encryption. */ -export const boxSeal = async (data: string, publicKey: string) => +export const boxSeal = (data: string, publicKey: string): Promise => inWorker() - ? ei._boxSeal(data, publicKey) + ? libsodium.boxSeal(data, publicKey) : sharedWorker().then((w) => w.boxSeal(data, publicKey)); /** * Decrypt the result of {@link boxSeal}. */ -export const boxSealOpen = async ( +export const boxSealOpen = ( encryptedData: string, - publicKey: string, - secretKey: string, -) => + keyPair: KeyPair, +): Promise => inWorker() - ? ei._boxSealOpen(encryptedData, publicKey, secretKey) + ? libsodium.boxSealOpen(encryptedData, keyPair) + : sharedWorker().then((w) => w.boxSealOpen(encryptedData, keyPair)); + +/** + * Variant of {@link boxSealOpen} that returns the decrypted bytes as it is + * (without encoding them to base64). + */ +export const boxSealOpenBytes = ( + encryptedData: string, + keyPair: KeyPair, +): Promise => + inWorker() + ? libsodium.boxSealOpenBytes(encryptedData, keyPair) : sharedWorker().then((w) => - w.boxSealOpen(encryptedData, publicKey, secretKey), + w.boxSealOpenBytes(encryptedData, keyPair), ); +/** + * Return a new randomly generated 128-bit salt (as a base64 string). + * + * The returned salt is suitable for use with {@link deriveKey}, and also as a + * general 128-bit salt. + */ +export const generateDeriveKeySalt = (): Promise => + inWorker() + ? libsodium.generateDeriveKeySalt() + : sharedWorker().then((w) => w.generateDeriveKeySalt()); + /** * Derive a key by hashing the given {@link passphrase} using Argon 2id. */ -export const deriveKey = async ( +export const deriveKey = ( passphrase: string, salt: string, opsLimit: number, memLimit: number, -) => +): Promise => inWorker() - ? ei._deriveKey(passphrase, salt, opsLimit, memLimit) + ? libsodium.deriveKey(passphrase, salt, opsLimit, memLimit) : sharedWorker().then((w) => w.deriveKey(passphrase, salt, opsLimit, memLimit), ); @@ -428,15 +495,34 @@ export const deriveKey = async ( /** * Derive a sensitive key from the given {@link passphrase}. */ -export const deriveSensitiveKey = async (passphrase: string, salt: string) => +export const deriveSensitiveKey = (passphrase: string): Promise => inWorker() - ? ei._deriveSensitiveKey(passphrase, salt) - : sharedWorker().then((w) => w.deriveSensitiveKey(passphrase, salt)); + ? libsodium.deriveSensitiveKey(passphrase) + : sharedWorker().then((w) => w.deriveSensitiveKey(passphrase)); /** - * Derive an interactive key from the given {@link passphrase}. + * Derive an key suitable for interactive use from the given {@link passphrase}. */ -export const deriveInteractiveKey = async (passphrase: string, salt: string) => +export const deriveInteractiveKey = ( + passphrase: string, +): Promise => inWorker() - ? ei._deriveInteractiveKey(passphrase, salt) - : sharedWorker().then((w) => w.deriveInteractiveKey(passphrase, salt)); + ? libsodium.deriveInteractiveKey(passphrase) + : sharedWorker().then((w) => w.deriveInteractiveKey(passphrase)); + +/** + * Derive a subkey of the given {@link key} using the specified parameters. + * + * @returns the bytes of the derived subkey. + */ +export const deriveSubKeyBytes = async ( + key: string, + subKeyLength: number, + subKeyID: number, + context: string, +): Promise => + inWorker() + ? libsodium.deriveSubKeyBytes(key, subKeyLength, subKeyID, context) + : sharedWorker().then((w) => + w.deriveSubKeyBytes(key, subKeyLength, subKeyID, context), + ); diff --git a/web/packages/base/crypto/libsodium.ts b/web/packages/base/crypto/libsodium.ts index b28b04720d..18129942d3 100644 --- a/web/packages/base/crypto/libsodium.ts +++ b/web/packages/base/crypto/libsodium.ts @@ -9,15 +9,22 @@ * To see where this code fits, see [Note: Crypto code hierarchy]. */ import { mergeUint8Arrays } from "ente-utils/array"; -import sodium, { type StateAddress } from "libsodium-wrappers-sumo"; -import type { - BytesOrB64, - EncryptedBlob, - EncryptedBlobB64, - EncryptedBlobBytes, - EncryptedBox, - EncryptedBoxB64, - EncryptedFile, +import sodium from "libsodium-wrappers-sumo"; +import { + deriveKeyInsufficientMemoryErrorMessage, + streamEncryptionChunkSize, + type BytesOrB64, + type DerivedKey, + type EncryptedBlob, + type EncryptedBlobB64, + type EncryptedBlobBytes, + type EncryptedBox, + type EncryptedBoxB64, + type EncryptedFile, + type InitChunkDecryptionResult, + type InitChunkEncryptionResult, + type KeyPair, + type SodiumStateAddress, } from "./types"; /** @@ -35,7 +42,7 @@ export const toB64 = async (input: Uint8Array) => { * * This is the converse of {@link toBase64}. */ -export const fromB64 = async (input: string) => { +export const fromB64 = async (input: string): Promise => { await sodium.ready; return sodium.from_base64(input, sodium.base64_variants.ORIGINAL); }; @@ -81,24 +88,6 @@ export const fromB64URLSafeNoPadding = async (input: string) => { return sodium.from_base64(input, sodium.base64_variants.URLSAFE_NO_PADDING); }; -/** - * Variant of {@link toB64URLSafeNoPadding} that works with {@link string} - * inputs. See also its sibling method {@link fromB64URLSafeNoPaddingString}. - */ -export const toB64URLSafeNoPaddingString = async (input: string) => { - await sodium.ready; - return toB64URLSafeNoPadding(sodium.from_string(input)); -}; - -/** - * Variant of {@link fromB64URLSafeNoPadding} that works with {@link strings}. See also - * its sibling method {@link toB64URLSafeNoPaddingString}. - */ -export const fromB64URLSafeNoPaddingString = async (input: string) => { - await sodium.ready; - return sodium.to_string(await fromB64URLSafeNoPadding(input)); -}; - /** * Convert a base64 string to the hex representation of the bytes that the base * 64 string encodes. @@ -111,12 +100,12 @@ export const toHex = async (input: string) => { }; /** - * Convert a hex string to the base 64 representation of the bytes that the hex + * Convert a hex string to the base64 representation of the bytes that the hex * string encodes. * * This is the inverse of {@link toHex}. */ -export const fromHex = async (input: string) => { +export const fromHex = async (input: string): Promise => { await sodium.ready; return await toB64(sodium.from_hex(input)); }; @@ -130,13 +119,29 @@ const bytes = async (bob: BytesOrB64) => typeof bob == "string" ? fromB64(bob) : bob; /** - * Generate a new key for use with the *Box encryption functions, and return its - * base64 string representation. + * Generate a new randomly generated 256-bit key for use as a general encryption + * key and return its base64 string representation. * - * This returns a new randomly generated 256-bit key suitable for being used - * with libsodium's secretbox APIs. + * From the architecture docs: + * + * > [`crypto_secretbox_keygen`](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) + * > is used to generate all random keys within the application. Your + * > `masterKey`, `recoveryKey`, `collectionKey`, `fileKey` are all 256-bit keys + * > generated using this API. + * + * {@link generateKey} can be contrasted with {@link generateBlobOrStreamKey} + * and can be thought of as a hypothetical "generateBoxKey". That is, the key + * returned by this function is suitable for being used with the *Box encryption + * functions (which eventually delegate to the libsodium's secretbox APIs). + * + * While this is a reasonable semantic distinction, in terms of implementation + * there is no difference: currently both {@link generateKey} (or the + * hypothetical "generateBoxKey") and {@link generateBlobOrStreamKey} produce + * 256-bits of entropy that does not have any ties to a particular algorithm. + * + * @returns A new randomly generated 256-bit key (as a base64 string). */ -export const generateBoxKey = async () => { +export const generateKey = async () => { await sodium.ready; return toB64(sodium.crypto_secretbox_keygen()); }; @@ -145,8 +150,8 @@ export const generateBoxKey = async () => { * Generate a new key for use with the *Blob or *Stream encryption functions, * and return its base64 string representation. * - * This returns a new randomly generated 256-bit key suitable for being used - * with libsodium's secretstream APIs. + * This returns a new randomly generated 256-bit key (as a base64 string) + * suitable for being used with libsodium's secretstream APIs. */ export const generateBlobOrStreamKey = async () => { await sodium.ready; @@ -157,7 +162,7 @@ export const generateBlobOrStreamKey = async () => { * Encrypt the given data using libsodium's secretbox APIs, using a randomly * generated nonce. * - * Use {@link decryptBox} to decrypt the result. + * Use {@link decryptBox} or {@link decryptBoxBytes} to decrypt the result. * * @param data The data to encrypt. * @@ -250,7 +255,7 @@ export const generateBlobOrStreamKey = async () => { * without chunking, whilst the *Stream routines first break it into * {@link streamEncryptionChunkSize} chunks. */ -export const encryptBoxB64 = async ( +export const encryptBox = async ( data: BytesOrB64, key: BytesOrB64, ): Promise => { @@ -270,7 +275,7 @@ export const encryptBoxB64 = async ( /** * Encrypt the given data using libsodium's secretstream APIs without chunking. * - * Use {@link decryptBlob} to decrypt the result. + * Use {@link decryptBlobBytes} to decrypt the result. * * @param data The data to encrypt. * @@ -282,7 +287,7 @@ export const encryptBoxB64 = async ( * * - See: https://doc.libsodium.org/secret-key_cryptography/secretstream */ -export const encryptBlob = async ( +export const encryptBlobBytes = async ( data: BytesOrB64, key: BytesOrB64, ): Promise => { @@ -303,14 +308,19 @@ export const encryptBlob = async ( }; /** - * A variant of {@link encryptBlob} that returns the both the encrypted data and - * decryption header as base64 strings. + * A higher level variant of {@link encryptBlobBytes} that returns the both the + * encrypted data and decryption header as base64 strings. + * + * This is the variant expected to serve majority of the public API use cases. */ -export const encryptBlobB64 = async ( +export const encryptBlob = async ( data: BytesOrB64, key: BytesOrB64, ): Promise => { - const { encryptedData, decryptionHeader } = await encryptBlob(data, key); + const { encryptedData, decryptionHeader } = await encryptBlobBytes( + data, + key, + ); return { encryptedData: await toB64(encryptedData), decryptionHeader: await toB64(decryptionHeader), @@ -318,17 +328,17 @@ export const encryptBlobB64 = async ( }; /** - * The various *Stream encryption functions break up the input into chunks of - * {@link streamEncryptionChunkSize} bytes during encryption (except the last - * chunk which can be smaller since a file would rarely align exactly to a - * {@link streamEncryptionChunkSize} multiple). + * Encrypt the provided JSON value (using {@link encryptBlob}) after converting + * it to a JSON string (and UTF-8 encoding it to obtain bytes). * - * The various *Stream decryption functions also assume that each potential - * chunk is {@link streamEncryptionChunkSize} long. - * - * This value of this constant is 4 MB (and is unlikely to change). + * Use {@link decryptMetadataJSON} to decrypt the result and convert it back to + * a JSON value. */ -export const streamEncryptionChunkSize = 4 * 1024 * 1024; +export const encryptMetadataJSON = ( + jsonValue: unknown, + key: BytesOrB64, +): Promise => + encryptBlob(new TextEncoder().encode(JSON.stringify(jsonValue)), key); /** * Encrypt the given data using libsodium's secretstream APIs after breaking it @@ -404,7 +414,9 @@ export const encryptStreamBytes = async ( * to subsequent calls to {@link encryptStreamChunk} along with the chunks's * contents. */ -export const initChunkEncryption = async (key: BytesOrB64) => { +export const initChunkEncryption = async ( + key: BytesOrB64, +): Promise => { await sodium.ready; const keyBytes = await bytes(key); const { state, header } = @@ -432,9 +444,9 @@ export const initChunkEncryption = async (key: BytesOrB64) => { */ export const encryptStreamChunk = async ( data: Uint8Array, - pushState: sodium.StateAddress, + pushState: SodiumStateAddress, isFinalChunk: boolean, -) => { +): Promise => { await sodium.ready; const tag = isFinalChunk ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL @@ -448,9 +460,9 @@ export const encryptStreamChunk = async ( }; /** - * Decrypt the result of {@link encryptBoxB64} and return the decrypted bytes. + * Decrypt the result of {@link encryptBox} and return the decrypted bytes. */ -export const decryptBox = async ( +export const decryptBoxBytes = async ( { encryptedData, nonce }: EncryptedBox, key: BytesOrB64, ): Promise => { @@ -463,17 +475,17 @@ export const decryptBox = async ( }; /** - * Variant of {@link decryptBox} that returns the data as a base64 string. + * Variant of {@link decryptBoxBytes} that returns the data as a base64 string. */ -export const decryptBoxB64 = ( +export const decryptBox = ( box: EncryptedBox, key: BytesOrB64, -): Promise => decryptBox(box, key).then(toB64); +): Promise => decryptBoxBytes(box, key).then(toB64); /** - * Decrypt the result of {@link encryptBlob} or {@link encryptBlobB64}. + * Decrypt the result of {@link encryptBlobBytes} or {@link encryptBlob}. */ -export const decryptBlob = async ( +export const decryptBlobBytes = async ( { encryptedData, decryptionHeader }: EncryptedBlob, key: BytesOrB64, ): Promise => { @@ -491,12 +503,29 @@ export const decryptBlob = async ( }; /** - * A variant of {@link decryptBlob} that returns the result as a base64 string. + * A variant of {@link decryptBlobBytes} that returns the result as a base64 + * string. */ -export const decryptBlobB64 = ( +export const decryptBlob = ( blob: EncryptedBlob, key: BytesOrB64, -): Promise => decryptBlob(blob, key).then(toB64); +): Promise => decryptBlobBytes(blob, key).then(toB64); + +/** + * Decrypt the result of {@link encryptMetadataJSON} and return the JSON value + * obtained by parsing the decrypted JSON string (which is obtained by UTF-8 + * decoding the decrypted bytes). + * + * Since TypeScript doesn't currently have a native JSON type, the returned + * value is casted as an `unknown`. + */ +export const decryptMetadataJSON = async ( + blob: EncryptedBlob, + key: BytesOrB64, +): Promise => + JSON.parse( + new TextDecoder().decode(await decryptBlobBytes(blob, key)), + ) as unknown; /** * Decrypt the result of {@link encryptStreamBytes}. @@ -504,7 +533,7 @@ export const decryptBlobB64 = ( export const decryptStreamBytes = async ( { encryptedData, decryptionHeader }: EncryptedFile, key: BytesOrB64, -) => { +): Promise => { await sodium.ready; const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull( await fromB64(decryptionHeader), @@ -550,7 +579,7 @@ export const decryptStreamBytes = async ( export const initChunkDecryption = async ( decryptionHeader: string, key: BytesOrB64, -) => { +): Promise => { await sodium.ready; const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull( await fromB64(decryptionHeader), @@ -573,8 +602,8 @@ export const initChunkDecryption = async ( */ export const decryptStreamChunk = async ( data: Uint8Array, - pullState: StateAddress, -) => { + pullState: SodiumStateAddress, +): Promise => { await sodium.ready; const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull( pullState, @@ -583,81 +612,65 @@ export const decryptStreamChunk = async ( return pullResult.message; }; -export interface B64EncryptionResult { - encryptedData: string; - key: string; - nonce: string; -} - -/** Deprecated, use {@link encryptBoxB64} instead */ -export async function encryptToB64(data: string, keyB64: string) { +/** + * Initialize and return new state that can be used to hash the chunks of data + * in a streaming manner. + * + * [Note: Chunked hashing] + * + * 1. Obtain a hash state using {@link chunkHashInit}. + * + * 2. Pass the hash state and the data chunk to {@link chunkHashUpdate}. + * + * 3. Finalize the hash state and obtain the hash using {@link chunkHashFinal}. + * + * @returns An opaque object representing the hash state. This can be passed + * (along with the data to hash) to {@link chunkHashUpdate}, and the final hash + * obtained using {@link chunkHashFinal}. + */ +export const chunkHashInit = async (): Promise => { await sodium.ready; - const encrypted = await encryptBoxB64(data, keyB64); - return { - encryptedData: encrypted.encryptedData, - key: keyB64, - nonce: encrypted.nonce, - } as B64EncryptionResult; -} - -export async function generateKeyAndEncryptToB64(data: string) { - await sodium.ready; - const key = sodium.crypto_secretbox_keygen(); - return await encryptToB64(data, await toB64(key)); -} - -export async function encryptUTF8(data: string, key: string) { - await sodium.ready; - const b64Data = await toB64(sodium.from_string(data)); - return await encryptToB64(b64Data, key); -} - -/** Deprecated, use {@link decryptBoxB64} instead. */ -export async function decryptB64( - encryptedData: string, - nonce: string, - keyB64: string, -) { - return decryptBoxB64({ encryptedData, nonce }, keyB64); -} - -/** Deprecated */ -export async function decryptToUTF8( - encryptedData: string, - nonce: string, - keyB64: string, -) { - await sodium.ready; - const decrypted = await decryptBox({ encryptedData, nonce }, keyB64); - return sodium.to_string(decrypted); -} - -export async function initChunkHashing() { - await sodium.ready; - const hashState = sodium.crypto_generichash_init( + return sodium.crypto_generichash_init( null, sodium.crypto_generichash_BYTES_MAX, ); - return hashState; -} +}; -export async function hashFileChunk( - hashState: sodium.StateAddress, +/** + * Update the hash state to incorporate the contents of the provided data chunk. + * + * See: [Note: Chunked hashing] + * + * @param hashState A hash state obtained using {@link chunkHashInit}. + * + * @param chunk The data (bytes) to hash. + */ +export const chunkHashUpdate = async ( + hashState: SodiumStateAddress, chunk: Uint8Array, -) { +) => { await sodium.ready; sodium.crypto_generichash_update(hashState, chunk); -} +}; -export async function completeChunkHashing(hashState: sodium.StateAddress) { +/** + * Finalize a hash state and return the hash it represents (as a base64 string). + * + * See: [Note: Chunked hashing] + * + * @param hashState A hash state obtained using {@link chunkHashInit} and fed + * chunks using {@link chunkHashUpdate}. + * + * @returns The hash of all the chunks (as a base64 string). + */ +export const chunkHashFinal = async (hashState: SodiumStateAddress) => { await sodium.ready; const hash = sodium.crypto_generichash_final( hashState, sodium.crypto_generichash_BYTES_MAX, ); - const hashString = toB64(hash); - return hashString; -} + return toB64(hash); +}; /** * Generate a new public/private keypair for use with public-key encryption @@ -699,24 +712,39 @@ export const boxSeal = async (data: string, publicKey: string) => { /** * Decrypt the result of {@link boxSeal}. * - * All parameters, and the result, are base64 string representations of the - * underlying data. + * All parameters are base64 string representations of the underlying data. The + * result is the bytes obtained by decryption. */ -export const boxSealOpen = async ( +export const boxSealOpenBytes = async ( encryptedData: string, - publicKey: string, - secretKey: string, -) => { + { publicKey, privateKey }: KeyPair, +): Promise => { await sodium.ready; - return toB64( - sodium.crypto_box_seal_open( - await fromB64(encryptedData), - await fromB64(publicKey), - await fromB64(secretKey), - ), + return sodium.crypto_box_seal_open( + await fromB64(encryptedData), + await fromB64(publicKey), + await fromB64(privateKey), ); }; +/** + * A variant of {@link boxSealOpenBytes} that returns the result as a base64 + * string. + */ +export const boxSealOpen = async (encryptedData: string, keyPair: KeyPair) => + toB64(await boxSealOpenBytes(encryptedData, keyPair)); + +/** + * Generate a new randomly generated 128-bit salt suitable for use with the key + * derivation functions ({@link deriveKey} and its variants). + * + * @returns The base64 representation of a randomly generated 128-bit salt. + */ +export const generateDeriveKeySalt = async () => { + await sodium.ready; + return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES)); +}; + /** * Derive a key by hashing the given {@link passphrase} using Argon 2id. * @@ -759,7 +787,7 @@ export const deriveKey = async ( /** * A variant of {@link deriveKey} with (dynamic) parameters for deriving - * sensitive keys (like the user's master key kek (key encryption key). + * sensitive keys (like the user's master key KEK (key encryption key). * * This function defers to {@link deriveKey} after choosing the most secure ops * and mem limits that the current device can handle. For details about these @@ -769,9 +797,13 @@ export const deriveKey = async ( * during the derivation (this information will be needed the user's other * clients to derive the same result). */ -export const deriveSensitiveKey = async (passphrase: string, salt: string) => { +export const deriveSensitiveKey = async ( + passphrase: string, +): Promise => { await sodium.ready; + const salt = await generateDeriveKeySalt(); + const desiredStrength = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE * sodium.crypto_pwhash_OPSLIMIT_SENSITIVE; @@ -800,13 +832,13 @@ export const deriveSensitiveKey = async (passphrase: string, salt: string) => { while (memLimit > minMemLimit) { try { const key = await deriveKey(passphrase, salt, opsLimit, memLimit); - return { key, opsLimit, memLimit }; + return { key, salt, opsLimit, memLimit }; } catch { opsLimit *= 2; memLimit /= 2; } } - throw new Error("Failed to derive key: Memory limit exceeded"); + throw new Error(deriveKeyInsufficientMemoryErrorMessage); }; /** @@ -815,38 +847,54 @@ export const deriveSensitiveKey = async (passphrase: string, salt: string) => { */ export const deriveInteractiveKey = async ( passphrase: string, - salt: string, -) => { +): Promise => { + const salt = await generateDeriveKeySalt(); const opsLimit = sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE; const memLimit = sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE; const key = await deriveKey(passphrase, salt, opsLimit, memLimit); - return { key, opsLimit, memLimit }; + return { key, salt, opsLimit, memLimit }; }; -export async function generateEncryptionKey() { - await sodium.ready; - return await toB64(sodium.crypto_kdf_keygen()); -} - -export async function generateSaltToDeriveKey() { - await sodium.ready; - return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES)); -} - -export async function generateSubKey( - key: string, +/** + * Derive a {@link subKeyID}-th subkey of length {@link subKeyLength} bytes by + * applying a KDF (Key Derivation Function) for the given {@link key} and the + * {@link context}. + * + * Multiple secret subkeys can be (deterministically) derived from a single + * high-entropy key. Knowledge of the derived key does not impact the security + * of the key from which it was derived, or of its potential sibling subkeys. + * + * See: https://doc.libsodium.org/key_derivation + * + * @param key The key whose subkey we are deriving. In the context of + * key derivation, this is usually referred to as the "master key", but we + * deemphasize that nomenclature to avoid confusion with the user's master key. + * + * @param subKeyLength The length of the required subkey. + * + * @param subKeyID An identifier of the subkey. + * + * @param context A short but otherwise arbitrary string (non-secret) used to + * separate domains in which the subkeys are going to be used. + * + * @returns The bytes of the subkey. + * + * Note that returning bytes is a bit unusual, usually we'd return the base64 + * string from functions. However, this particular function is used in only one + * place in our code, and so we adapt the interface for its convenience. + */ +export const deriveSubKeyBytes = async ( + key: BytesOrB64, subKeyLength: number, subKeyID: number, context: string, -) { +): Promise => { await sodium.ready; - return await toB64( - sodium.crypto_kdf_derive_from_key( - subKeyLength, - subKeyID, - context, - await fromB64(key), - ), + return sodium.crypto_kdf_derive_from_key( + subKeyLength, + subKeyID, + context, + await bytes(key), ); -} +}; diff --git a/web/packages/base/crypto/types.ts b/web/packages/base/crypto/types.ts index d79a32f85e..8b82a2aef8 100644 --- a/web/packages/base/crypto/types.ts +++ b/web/packages/base/crypto/types.ts @@ -1,3 +1,40 @@ +/** + * @file types shared between the public API (main thread) and implementation + * (worker thread). Also some constants, but no code. + */ +import { type StateAddress } from "libsodium-wrappers-sumo"; + +/** + * An opaque object meant to be threaded through various functions that deal + * with resumable chunk based processing. + */ +export type SodiumStateAddress = StateAddress; + +/** + * The various *Stream encryption functions break up the input into chunks of + * {@link streamEncryptionChunkSize} bytes during encryption (except the last + * chunk which can be smaller since a file would rarely align exactly to a + * {@link streamEncryptionChunkSize} multiple). + * + * The various *Stream decryption functions also assume that each potential + * chunk is {@link streamEncryptionChunkSize} long. + * + * This value of this constant is 4 MB (and is unlikely to change). + */ +export const streamEncryptionChunkSize = 4 * 1024 * 1024; + +/** + * The message of the {@link Error} that is thrown by {@link deriveSensitiveKey} + * if we could not find acceptable ops and mem limit combinations without + * exceeded the maximum mem limit. + * + * Generally, this indicates that the current device is not powerful enough to + * perform the key derivation. This is rare for computers, but can happen with + * older mobile devices with too little RAM. + */ +export const deriveKeyInsufficientMemoryErrorMessage = + "Failed to derive key (insufficient memory)"; + /** * Data provided either as bytes ({@link Uint8Array}) or their base64 string * representation. @@ -129,3 +166,78 @@ export interface EncryptedFile { */ decryptionHeader: string; } + +/** + * An object returned by the init function of chunked encryption routines. + */ +export interface InitChunkEncryptionResult { + /** + * A base64 string containing the decryption header. + * + * While the exact contents of the header are libsodium's internal details, + * it effectively contains a random nonce generated by libsodium. It does + * not need to be secret, but it is required to decrypt the data. + */ + decryptionHeader: string; + /** + * An opaque value that refers to the internal state used by the resumable + * calls in the encryption sequence. + */ + pushState: SodiumStateAddress; +} + +/** + * An object returned by the init function of chunked decryption routines. + */ +export interface InitChunkDecryptionResult { + /** + * An opaque value that refers to the internal state used by the resumable + * calls in the decryption sequence. + */ + pullState: SodiumStateAddress; + /** + * The expected size of each chunk. + */ + decryptionChunkSize: number; +} + +/** + * A pair of public and private keys. + */ +export interface KeyPair { + /** + * The public key of the keypair, as a base64 encoded string. + */ + publicKey: string; + /** + * The private key of the keypair, as a base64 encoded string. + * + * Some places also refer to it as the "secret key". + * + * See: [Note: privateKey and secretKey]. + */ + privateKey: string; +} + +/** + * A key derived from a user provided passphrase, and the various attributes + * that were used during the key derivation. + */ +export interface DerivedKey { + /** + * The newly derived key itself, as a base64 encoded string. + */ + key: string; + /** + * The randomly generated salt (as a base64 string) that was used when deriving the key. + */ + salt: string; + /** + * opsLimit used during key derivation. + */ + opsLimit: number; + /** + * memLimit used during key derivation. + * */ + memLimit: number; +} diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 8e67927ecd..bc25a7c0ad 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -1,101 +1,51 @@ import { expose } from "comlink"; import { logUnhandledErrorsAndRejectionsInWorker } from "ente-base/log-web"; -import type { StateAddress } from "libsodium-wrappers-sumo"; -import * as ei from "./ente-impl"; import * as libsodium from "./libsodium"; /** - * A web worker that exposes some of the functions defined in either the Ente - * specific layer (ente-base/crypto) or the libsodium layer - * (ente-base/crypto/libsodium.ts). + * A web worker that exposes the functions defined by libsodium.ts. * * See: [Note: Crypto code hierarchy]. * * Note: Keep these methods logic free. They are meant to be trivial proxies. */ export class CryptoWorker { - toB64 = ei._toB64; - toB64URLSafe = ei._toB64URLSafe; - fromB64 = ei._fromB64; - toHex = ei._toHex; - fromHex = ei._fromHex; - generateBoxKey = ei._generateBoxKey; - generateBlobOrStreamKey = ei._generateBlobOrStreamKey; - encryptBoxB64 = ei._encryptBoxB64; - encryptThumbnail = ei._encryptThumbnail; - encryptBlobB64 = ei._encryptBlobB64; - encryptStreamBytes = ei._encryptStreamBytes; - initChunkEncryption = ei._initChunkEncryption; - encryptStreamChunk = ei._encryptStreamChunk; - encryptMetadataJSON_New = ei._encryptMetadataJSON_New; - encryptMetadataJSON = ei._encryptMetadataJSON; - decryptBox = ei._decryptBox; - decryptBoxB64 = ei._decryptBoxB64; - decryptBlob = ei._decryptBlob; - decryptBlobB64 = ei._decryptBlobB64; - decryptThumbnail = ei._decryptThumbnail; - decryptStreamBytes = ei._decryptStreamBytes; - initChunkDecryption = ei._initChunkDecryption; - decryptStreamChunk = ei._decryptStreamChunk; - decryptMetadataJSON_New = ei._decryptMetadataJSON_New; - decryptMetadataJSON = ei._decryptMetadataJSON; - generateKeyPair = ei._generateKeyPair; - boxSeal = ei._boxSeal; - boxSealOpen = ei._boxSealOpen; - deriveKey = ei._deriveKey; - deriveSensitiveKey = ei._deriveSensitiveKey; - deriveInteractiveKey = ei._deriveInteractiveKey; - - // TODO: -- AUDIT BELOW -- - - async initChunkHashing() { - return libsodium.initChunkHashing(); - } - - async hashFileChunk(hashState: StateAddress, chunk: Uint8Array) { - return libsodium.hashFileChunk(hashState, chunk); - } - - async completeChunkHashing(hashState: StateAddress) { - return libsodium.completeChunkHashing(hashState); - } - - async decryptB64(data: string, nonce: string, key: string) { - return libsodium.decryptB64(data, nonce, key); - } - - async decryptToUTF8(data: string, nonce: string, key: string) { - return libsodium.decryptToUTF8(data, nonce, key); - } - - async encryptToB64(data: string, key: string) { - return libsodium.encryptToB64(data, key); - } - - async generateKeyAndEncryptToB64(data: string) { - return libsodium.generateKeyAndEncryptToB64(data); - } - - async encryptUTF8(data: string, key: string) { - return libsodium.encryptUTF8(data, key); - } - - async generateEncryptionKey() { - return libsodium.generateEncryptionKey(); - } - - async generateSaltToDeriveKey() { - return libsodium.generateSaltToDeriveKey(); - } - - async generateSubKey( - key: string, - subKeyLength: number, - subKeyID: number, - context: string, - ) { - return libsodium.generateSubKey(key, subKeyLength, subKeyID, context); - } + toB64 = libsodium.toB64; + fromB64 = libsodium.fromB64; + toB64URLSafe = libsodium.toB64URLSafe; + toB64URLSafeNoPadding = libsodium.toB64URLSafeNoPadding; + fromB64URLSafeNoPadding = libsodium.fromB64URLSafeNoPadding; + toHex = libsodium.toHex; + fromHex = libsodium.fromHex; + generateKey = libsodium.generateKey; + generateBlobOrStreamKey = libsodium.generateBlobOrStreamKey; + encryptBox = libsodium.encryptBox; + encryptBlob = libsodium.encryptBlob; + encryptBlobBytes = libsodium.encryptBlobBytes; + encryptMetadataJSON = libsodium.encryptMetadataJSON; + encryptStreamBytes = libsodium.encryptStreamBytes; + initChunkEncryption = libsodium.initChunkEncryption; + encryptStreamChunk = libsodium.encryptStreamChunk; + decryptBox = libsodium.decryptBox; + decryptBoxBytes = libsodium.decryptBoxBytes; + decryptBlob = libsodium.decryptBlob; + decryptBlobBytes = libsodium.decryptBlobBytes; + decryptMetadataJSON = libsodium.decryptMetadataJSON; + decryptStreamBytes = libsodium.decryptStreamBytes; + initChunkDecryption = libsodium.initChunkDecryption; + decryptStreamChunk = libsodium.decryptStreamChunk; + chunkHashInit = libsodium.chunkHashInit; + chunkHashUpdate = libsodium.chunkHashUpdate; + chunkHashFinal = libsodium.chunkHashFinal; + generateKeyPair = libsodium.generateKeyPair; + boxSeal = libsodium.boxSeal; + boxSealOpen = libsodium.boxSealOpen; + boxSealOpenBytes = libsodium.boxSealOpenBytes; + generateDeriveKeySalt = libsodium.generateDeriveKeySalt; + deriveKey = libsodium.deriveKey; + deriveSensitiveKey = libsodium.deriveSensitiveKey; + deriveInteractiveKey = libsodium.deriveInteractiveKey; + deriveSubKeyBytes = libsodium.deriveSubKeyBytes; } expose(CryptoWorker); diff --git a/web/packages/base/http.ts b/web/packages/base/http.ts index 6a12185c56..d926037445 100644 --- a/web/packages/base/http.ts +++ b/web/packages/base/http.ts @@ -1,8 +1,9 @@ +import { desktopAppVersion, isDesktop } from "ente-base/app"; import { wait } from "ente-utils/promise"; -import { z } from "zod"; +import { z } from "zod/v4"; import { clientPackageName } from "./app"; -import { ensureAuthToken } from "./local-user"; import log from "./log"; +import { ensureAuthToken } from "./token"; /** * Return headers that should be passed alongwith (almost) all authenticated @@ -14,16 +15,19 @@ import log from "./log"; export const authenticatedRequestHeaders = async () => ({ "X-Auth-Token": await ensureAuthToken(), "X-Client-Package": clientPackageName, + ...(isDesktop && { "X-Client-Version": desktopAppVersion }), }); /** * Return headers that should be passed alongwith (almost) all unauthenticated - * `fetch` calls that we make to our API servers. + * `fetch` calls that we make to our remotes like our API servers (museum), or + * to pre-signed URLs that are handled by the S3 storage buckets themselves. * * - The client package name. */ export const publicRequestHeaders = () => ({ "X-Client-Package": clientPackageName, + ...(isDesktop && { "X-Client-Version": desktopAppVersion }), }); /** @@ -83,7 +87,7 @@ export class HTTPError extends Error { super(`HTTP ${res.status} ${res.statusText} (${url.pathname})`); const requestID = res.headers.get("x-request-id"); - const details = { url: url.href, ...(requestID ? { requestID } : {}) }; + const details = { url: url.href, ...(requestID && { requestID }) }; // Cargo culted from // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types @@ -137,6 +141,8 @@ export const isHTTP401Error = (e: unknown) => * Return `true` if this is an error because of a HTTP failure response returned * by museum with the given "code" and HTTP status. * + * > The function is async because it needs to parse the payload. + * * For some known set of errors, museum returns a payload of the form * * {"code":"USER_NOT_REGISTERED","message":"User is not registered"} @@ -164,16 +170,46 @@ export const isMuseumHTTPError = async ( return false; }; +interface RetryAsyncOperationOpts { + /** + * An optional modification to the default wait times between retries. + * + * - default 1 orig + 3 retries (2s, 5s, 10s) + * - "background" 1 orig + 3 retries (5s, 25s, 120s) + * + * default is fine for most operations, including interactive ones where we + * don't want to wait too long before giving up. "background" is suitable + * for non-interactive operations where we can wait for longer (thus better + * handle remote hiccups) without degrading the user's experience. + */ + retryProfile?: "background"; + /** + * An optional function that is called with the corresponding error whenever + * {@link op} rejects. It should throw the error if the retries should + * immediately be aborted. + */ + abortIfNeeded?: (error: unknown) => void; +} + /** - * Retry a async operation like a HTTP request 3 (+ 1 original) times with - * exponential backoff. + * Retry a async operation on failure up to 4 times (1 original + 3 retries) + * with exponential backoff. + * + * [Note: Retries of network requests should be idempotent] + * + * When dealing with network requests, avoid using this function directly, use + * one of its wrappers like {@link retryEnsuringHTTPOk} instead. Those wrappers + * ultimately use this function only, and there is nothing wrong with this + * function generally, however since this function allows retrying arbitrary + * promises, it is easy accidentally try and attempt retries of non-idemponent + * requests, while the more restricted API of {@link retryEnsuringHTTPOk} and + * other {@link HTTPRequestRetrier}s makes such misuse less likely. * * @param op A function that performs the operation, returning the promise for * its completion. * - * @param abortIfNeeded An optional function that is called with the - * corresponding error whenever {@link op} rejects. It should throw the error if - * the retries should immediately be aborted. + * @param opts Optional tweaks to the default implementation. See + * {@link RetryAsyncOperationOpts}. * * @returns A promise that fulfills with to the result of a first successfully * fulfilled promise of the 4 (1 + 3) attempts, or rejects with the error @@ -182,9 +218,13 @@ export const isMuseumHTTPError = async ( */ export const retryAsyncOperation = async ( op: () => Promise, - abortIfNeeded?: (error: unknown) => void, + opts?: RetryAsyncOperationOpts, ): Promise => { - const waitTimeBeforeNextTry = [2000, 5000, 10000]; + const { retryProfile, abortIfNeeded } = opts ?? {}; + const waitTimeBeforeNextTry = + retryProfile == "background" + ? [10000, 30000, 120000] + : [2000, 5000, 10000]; while (true) { try { @@ -201,35 +241,59 @@ export const retryAsyncOperation = async ( } }; +/** + * A function that wraps the request(s) in retries if needed. + * + * See {@link retryEnsuringHTTPOk} for the canonical example. This typedef is to + * allow us to talk about and pass functions that behave similar to + * {@link retryEnsuringHTTPOk}, but perhaps with other additional checks. + * + * See also: [Note: Retries of network requests should be idempotent] + */ +export type HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => Promise; + +type HTTPRequestRetrierOpts = Pick; + /** * A helper function to adapt {@link retryAsyncOperation} for HTTP fetches. * - * This will ensure that the HTTP operation returning a non-200 OK status (as - * matched by {@link ensureOk}) is also counted as an error when considering if - * a request should be retried. + * This extends {@link retryAsyncOperation} by treating any non-200 OK status + * (as matched by {@link ensureOk}) as an error that should be retried. */ -export const retryEnsuringHTTPOk = (request: () => Promise) => +export const retryEnsuringHTTPOk: HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => retryAsyncOperation(async () => { const r = await request(); ensureOk(r); return r; - }); + }, opts); /** - * A helper function to {@link retryAsyncOperation} for HTTP fetches, but - * considering any 4xx HTTP responses as irrecoverable failures. + * A helper function to adapt {@link retryAsyncOperation} for HTTP fetches, but + * treating any 4xx HTTP responses as irrecoverable failures. * * This is similar to {@link retryEnsuringHTTPOk}, except it stops retrying if * remote responds with a 4xx HTTP status. */ -export const retryEnsuringHTTPOkOr4xx = (request: () => Promise) => +export const retryEnsuringHTTPOkOr4xx: HTTPRequestRetrier = ( + request: () => Promise, + opts?: HTTPRequestRetrierOpts, +) => retryAsyncOperation( async () => { const r = await request(); ensureOk(r); return r; }, - (e) => { - if (isHTTP4xxError(e)) throw e; + { + ...opts, + abortIfNeeded(e) { + if (isHTTP4xxError(e)) throw e; + }, }, ); diff --git a/web/packages/base/i18n.ts b/web/packages/base/i18n.ts index 6bbcf3a734..90c6cfce1c 100644 --- a/web/packages/base/i18n.ts +++ b/web/packages/base/i18n.ts @@ -35,6 +35,7 @@ export const supportedLocales = [ "vi-VN" /* Vietnamese */, "ja-JP" /* Japanese */, "ar-SA" /* Arabic */, + "tr-TR" /* Turkish */, ] as const; /** The type of {@link supportedLocales}. */ @@ -198,6 +199,8 @@ const closestSupportedLocale = ( return "ja-JP"; } else if (ls.startsWith("ar")) { return "ar-SA"; + } else if (ls.startsWith("tr")) { + return "tr-TR"; } } diff --git a/web/packages/base/kv.ts b/web/packages/base/kv.ts index 5fa20fb4ce..df90aca537 100644 --- a/web/packages/base/kv.ts +++ b/web/packages/base/kv.ts @@ -136,7 +136,7 @@ export const getKV = async (key: string) => { return db.get("kv", key); }; -export const _getKV = async ( +const _getKV = async ( key: string, type: string, ): Promise => { diff --git a/web/packages/base/local-user.ts b/web/packages/base/local-user.ts deleted file mode 100644 index 9bde1583a4..0000000000 --- a/web/packages/base/local-user.ts +++ /dev/null @@ -1,63 +0,0 @@ -// TODO: This file belongs to the accounts package - -import { z } from "zod"; -import { getKVS } from "./kv"; - -// TODO: During login the only field present is email. Which makes this -// optionality indicated by these types incorrect. -const LocalUser = z.object({ - /** The user's ID. */ - id: z.number(), - /** The user's email. */ - email: z.string(), - /** - * The user's (plaintext) auth token. - * - * It is used for making API calls on their behalf, by passing this token as - * the value of the X-Auth-Token header in the HTTP request. - */ - token: z.string(), -}); - -/** Locally available data for the logged in user */ -export type LocalUser = z.infer; - -/** - * Return the logged-in user, if someone is indeed logged in. Otherwise return - * `undefined`. - * - * The user's data is stored in the browser's localStorage. - */ -export const localUser = (): LocalUser | undefined => { - // TODO: duplicate of getData("user") - const s = localStorage.getItem("user"); - if (!s) return undefined; - return LocalUser.parse(JSON.parse(s)); -}; - -/** - * A wrapper over {@link localUser} with that throws if no one is logged in. - */ -export const ensureLocalUser = (): LocalUser => { - const user = localUser(); - if (!user) throw new Error("Not logged in"); - return user; -}; - -/** - * Return the user's auth token, or throw an error. - * - * The user's auth token is stored in KV DB after they have successfully logged - * in. This function returns that saved auth token. - * - * If no such token is found (which should only happen if the user is not logged - * in), then it throws an error. - * - * The underlying data is stored in IndexedDB, and can be accessed from web - * workers. - */ -export const ensureAuthToken = async () => { - const token = await getKVS("token"); - if (!token) throw new Error("Not logged in"); - return token; -}; diff --git a/web/packages/base/locales/ar-SA/translation.json b/web/packages/base/locales/ar-SA/translation.json index 0a962e30a2..b0863329d0 100644 --- a/web/packages/base/locales/ar-SA/translation.json +++ b/web/packages/base/locales/ar-SA/translation.json @@ -32,6 +32,7 @@ "set_password": "تعيين كلمة المرور", "sign_in": "تسجيل الدخول", "incorrect_password": "كلمة المرور غير صحيحة", + "incorrect_password_or_no_account": "كلمة المرور غير صحيحة أو البريد الإلكتروني غير مسجل", "pick_password_hint": "الرجاء إدخال كلمة المرور التي يمكننا استخدامها لتشفير بياناتك", "pick_password_caution": "نحن لا نخزن كلمة مرورك، لذا إذا نسيتها، لن نتمكن من مساعدتك في استرداد بياناتك دون مفتاح الاسترداد.", "key_generation_in_progress": "جارٍ توليد مفاتيح التشفير...", @@ -39,11 +40,12 @@ "referral_source_hint": "كيف سمعت عن Ente؟ (اختياري)", "referral_source_info": "نحن لا نتتبع عمليات تثبيت التطبيق، سيكون من المفيد لنا أن تخبرنا أين وجدتنا!", "password_mismatch_error": "كلمات المرور غير متطابقة", + "show_or_hide_password": "إظهار أو إخفاء كلمة المرور", "welcome_to_ente_title": "مرحبًا بك في ", "welcome_to_ente_subtitle": "تخزين الصور ومشاركتها بشكل مشفر من طرف إلى طرف", "new_album": "ألبوم جديد", "create_albums": "إنشاء ألبومات", - "enter_album_name": "اسم الألبوم", + "album_name": "اسم الألبوم", "close": "إغلاق", "yes": "نعم", "no": "لا", @@ -56,8 +58,8 @@ "add_photos_count": "إضافة {{count, number}} عناصر", "select_photos": "تحديد الصور", "file_upload": "تحميل الملف", - "preparing": "", - "processed_counts": "", + "preparing": "جاري التحضير", + "processed_counts": "{{count, number}} / {{total, number}}", "upload_reading_metadata_files": "", "upload_cancelling": "إلغاء التحميلات المتبقية", "upload_done": "", @@ -529,9 +531,9 @@ "ml_consent_description": "

إذا قمت بتمكين تعلم الآلة، سيقوم Ente باستخراج معلومات مثل هندسة الوجه من الملفات، بما في ذلك تلك التي تمت مشاركتها معك.

سيحدث هذا على جهازك، وسيتم تشفير أي معلومات بيومترية تم إنشاؤها تشفيرًا تامًا من طرف إلى طرف.

يرجى النقر هنا لمزيد من التفاصيل حول هذه الميزة في سياسة الخصوصية الخاصة بنا

", "ml_consent_confirmation": "أنا أفهم، وأرغب في تمكين تعلم الآلة", "labs": "المختبرات", - "passphrase_strength_weak": "قوة كلمة المرور: ضعيفة", - "passphrase_strength_moderate": "قوة كلمة المرور: متوسطة", - "passphrase_strength_strong": "قوة كلمة المرور: قوية", + "password_strength_weak": "قوة كلمة المرور: ضعيفة", + "password_strength_moderate": "قوة كلمة المرور: متوسطة", + "password_strength_strong": "قوة كلمة المرور: قوية", "preferences": "التفضيلات", "language": "اللغة", "advanced": "متقدم", @@ -624,6 +626,7 @@ "faster_upload_description": "توجيه التحميلات عبر خوادم قريبة", "open_ente_on_startup": "فتح Ente عند بدء التشغيل", "cast_album_to_tv": "تشغيل الألبوم على التلفزيون", + "cast_to_tv": "تشغيل على التلفاز", "enter_cast_pin_code": "أدخل الرمز الذي تراه على التلفزيون أدناه لإقران هذا الجهاز.", "code": "الرمز", "pair_device_to_tv": "إقران الأجهزة", @@ -674,5 +677,11 @@ "theme": "المظهر", "system": "النظام", "light": "فاتح", - "dark": "داكن" + "dark": "داكن", + "streamable_videos": "فيديوهات قابلة للبث", + "processing_videos_status": "معالجة الفيديوهات...", + "share_favorites": "مشاركة المفضلات", + "person_favorites": "مفضلات {{name}}", + "shared_favorites": "", + "added_by_name": "أضيفت بواسطة {{name}}" } diff --git a/web/packages/base/locales/be-BY/translation.json b/web/packages/base/locales/be-BY/translation.json index a9bd30fe39..8f0f7ced5e 100644 --- a/web/packages/base/locales/be-BY/translation.json +++ b/web/packages/base/locales/be-BY/translation.json @@ -32,6 +32,7 @@ "set_password": "Задаць пароль", "sign_in": "Увайсці", "incorrect_password": "Няправільны пароль", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "Закрыць", "yes": "", "no": "Не", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/bg-BG/translation.json b/web/packages/base/locales/bg-BG/translation.json index 37bded86e6..833d459e9a 100644 --- a/web/packages/base/locales/bg-BG/translation.json +++ b/web/packages/base/locales/bg-BG/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ca-ES/translation.json b/web/packages/base/locales/ca-ES/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/ca-ES/translation.json +++ b/web/packages/base/locales/ca-ES/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/cs-CZ/translation.json b/web/packages/base/locales/cs-CZ/translation.json index 16761bd3e8..c94b18d204 100644 --- a/web/packages/base/locales/cs-CZ/translation.json +++ b/web/packages/base/locales/cs-CZ/translation.json @@ -1,8 +1,8 @@ { "intro_slide_1_title": "Soukromé zálohy
pro vaše vzpomínky", - "intro_slide_1": "", - "intro_slide_2_title": "", - "intro_slide_2": "", + "intro_slide_1": "End-to-end šifrováno ve výchozím nastavení", + "intro_slide_2_title": "Bezpečně skladovaný
v úkryt", + "intro_slide_2": "Navržen pro prežití", "intro_slide_3_title": "Dostupné
všude", "intro_slide_3": "Android, iOS, Web, počítač", "login": "Přihlášení", @@ -19,7 +19,7 @@ "verification_code": "Ověřovací kód", "resend_code": "Opětovné odeslání kódu", "verify": "Ověřit", - "send_otp": "", + "send_otp": "Odeslat OTP", "generic_error": "Něco se pokazilo", "generic_error_retry": "Něco se pokazilo. Zkuste to, prosím, znovu", "invalid_code_error": "Neplatný ověřovací kód", @@ -32,6 +32,7 @@ "set_password": "Nastavit heslo", "sign_in": "Přihlásit se", "incorrect_password": "Nesprávné heslo", + "incorrect_password_or_no_account": "", "pick_password_hint": "Zadejte heslo, kterým můžeme zašifrovat Vaše data", "pick_password_caution": "Vaše heslo neukládáme, takže pokud ho zapomenete, nebudeme moci pomoci obnovit vaše data bez obnovovacího klíče.", "key_generation_in_progress": "Generování šifrovacích klíčů...", @@ -39,29 +40,30 @@ "referral_source_hint": "Jak jste se dozvěděli o Ente? (volitelné)", "referral_source_info": "Ne sledujeme instalace aplikace. Pomůže nám, když nám sdělíte, kde jste nás našli!", "password_mismatch_error": "Hesla se neshodují", + "show_or_hide_password": "", "welcome_to_ente_title": "Vítejte v ", - "welcome_to_ente_subtitle": "", - "new_album": "Nový album", + "welcome_to_ente_subtitle": "End to end šifrované úložiště fotek a sdílení", + "new_album": "Nové album", "create_albums": "Vytvořit alba", - "enter_album_name": "Název alba", + "album_name": "Název alba", "close": "Zavřít", "yes": "Ano", "no": "Ne", "nothing_here": "Nic tady ještě není", "upload": "Nahrát", "import": "Importovat", - "add_photos": "Přidat fotografie", + "add_photos": "Přidat fotky", "add_more_photos": "Přidat další fotky", "add_photos_count_one": "Přidat 1 položku", "add_photos_count": "Přidat {{count, number}} předměty", - "select_photos": "Vybrat fotografii", + "select_photos": "Vybrat fotky", "file_upload": "Nahrát soubor", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "Příprava", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Čtení souborů metadat", "upload_cancelling": "Ruší se zbývající nahrávání", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} nahráno", + "upload_skipped": "{{count, number}} přeskočeno", "initial_load_delay_warning": "První načitení může chvíli trvat", "no_account": "Nemáte účet", "existing_account": "Již máte účet", @@ -87,22 +89,22 @@ "toggle_live": "Přepnout živý", "toggle_audio": "Přepnout zvuk", "toggle_favorite": "Přepnout oblíbené", - "toggle_archive": "", + "toggle_archive": "Přepnout archiv", "view_info": "Zobrazit informace", "copy_as_png": "Kopírovat jako PNG", "toggle_fullscreen": "Přepnout na celou obrazovku", "exit_fullscreen": "Ukončit režim celé obrazovky", "go_fullscreen": "Přepnout na celou obrazovku", "zoom": "Přiblížení", - "play": "", - "pause": "", + "play": "Přehrát", + "pause": "Pozastavit", "previous": "Předchozí", "next": "Další", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Vyhledávání videa", + "quality": "Kvalita", + "auto": "Automatická", + "original": "Původní", + "speed": "Rychlost", "title_photos": "Ente fotky", "title_auth": "Ente Auth", "title_accounts": "Ente účty", @@ -308,8 +310,8 @@ "faq": "Často kladené dotazy", "takeout_hint": "", "destination": "Cíl", - "start": "", - "last_export_time": "", + "start": "Spustit", + "last_export_time": "Čas posledního exportu", "export_again": "", "local_storage_not_accessible": "", "email_already_taken": "E-mail je již obsazen", @@ -322,16 +324,16 @@ "retry_failed_uploads": "", "thumbnail_generation_failed": "", "thumbnail_generation_failed_hint": "Tyto soubory byly nahrány, ale bohužel jsme pro ně nemohli vygenerovat titulky.", - "unsupported_files": "", - "unsupported_files_hint": "", + "unsupported_files": "Nepodporované soubory", + "unsupported_files_hint": "Ente zatím nepodporuje tyto formáty souborů", "blocked_uploads": "", "blocked_uploads_hint": "", "large_files": "Velké soubory", "large_files_hint": "", "insufficient_storage": "", "insufficient_storage_hint": "", - "uploads_in_progress": "", - "successful_uploads": "", + "uploads_in_progress": "Probíhá nahrávání", + "successful_uploads": "Úspěšně nahrané", "upload_to_album": "Nahrát do alba", "add_to_album": "Přidat do alba", "move_to_album": "Přesunout do alba", @@ -410,17 +412,17 @@ "viewers": "Prohlížející", "add_more": "Přidat další", "or_add_existing": "", - "not_found": "", + "not_found": "404 - Nenalezeno", "link_expired": "Platnost odkazu vypršela", "link_expired_message": "", "manage_link": "", "link_request_limit_exceeded": "", "allow_downloads": "Povolit stahování", - "allow_adding_photos": "", + "allow_adding_photos": "Povolit přidávání fotek", "allow_adding_photos_hint": "", - "device_limit": "", + "device_limit": "Limit zařízení", "none": "", - "link_expiry": "", + "link_expiry": "Platnost odkazu", "never": "Nikdy", "after_time": { "hour": "Po hodině", @@ -433,8 +435,8 @@ "done": "Hotovo", "share_link_section_title": "Nebo sdílet odkaz", "remove_link": "Odstranit odkaz", - "create_public_link": "", - "public_link_created": "", + "create_public_link": "Vytvořit veřejný odkaz", + "public_link_created": "Veřejný odkaz vytvořen", "public_link_enabled": "", "collect_photos": "", "disable_file_download": "", @@ -496,28 +498,28 @@ "view_logs_message": "", "weak_device_hint": "", "drag_and_drop_hint": "", - "authenticate": "", + "authenticate": "Ověřit", "uploaded_to_single_collection": "", "uploaded_to_separate_collections": "", - "nevermind": "", + "nevermind": "Nevadí", "update_available": "Je k dispozici aktualizace", "update_installable_message": "", "install_now": "Nainstalovat nyní", "install_on_next_launch": "Instalovat při dalším spuštění", "update_available_message": "", "download_and_install": "Stáhnout a nainstalovat", - "ignore_this_version": "", + "ignore_this_version": "Ignorovat tuto verzi", "today": "Dnes", "yesterday": "Včera", "enter_name": "Zadejte jméno", "uploader_name_hint": "", "name_placeholder": "Název...", "more_details": "Další podrobnosti", - "ml_search": "", + "ml_search": "Strojové učení", "ml_search_description": "", "ml_search_footnote": "", - "indexing": "", - "processed": "", + "indexing": "Indexování", + "processed": "Zpracováno", "indexing_status_running": "", "indexing_status_fetching": "", "indexing_status_scheduled": "", @@ -528,17 +530,17 @@ "ml_consent_title": "", "ml_consent_description": "", "ml_consent_confirmation": "", - "labs": "", - "passphrase_strength_weak": "Síla hesla: Slabá", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "Síla hesla: Silná", + "labs": "Experimentální", + "password_strength_weak": "Síla hesla: Slabá", + "password_strength_moderate": "", + "password_strength_strong": "Síla hesla: Silná", "preferences": "Předvolby", "language": "Jazyk", "advanced": "Pokročilé", "export_directory_does_not_exist": "", "export_directory_does_not_exist_message": "", "storage_unit": { - "b": "", + "b": "B", "kb": "kB", "mb": "MB", "gb": "GB", @@ -599,15 +601,15 @@ "confirm_editor_close_message": "", "brightness": "Jas", "contrast": "Kontrast", - "saturation": "", + "saturation": "Sytost", "blur": "Rozostření", - "transform": "", + "transform": "Transformovat", "crop": "Oříznout", - "aspect_ratio": "", + "aspect_ratio": "Poměr stran", "square": "Čtverec", - "freehand": "", + "freehand": "Volný", "apply_crop": "", - "rotation": "", + "rotation": "Otočení", "rotate_left": "Otočit doleva", "rotate_right": "Otočit doprava", "flip": "Překlopit", @@ -624,9 +626,10 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Přehrát album v televizi", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "Kód", - "pair_device_to_tv": "", + "pair_device_to_tv": "Párovat zařízení", "tv_not_found": "", "cast_auto_pair": "", "cast_auto_pair_description": "", @@ -674,5 +677,11 @@ "theme": "Téma", "system": "Systém", "light": "Světlý", - "dark": "Tmavý" + "dark": "Tmavý", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/da-DK/translation.json b/web/packages/base/locales/da-DK/translation.json index 885499e046..e66368c264 100644 --- a/web/packages/base/locales/da-DK/translation.json +++ b/web/packages/base/locales/da-DK/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/de-DE/translation.json b/web/packages/base/locales/de-DE/translation.json index a3893387fa..7dfbd83e82 100644 --- a/web/packages/base/locales/de-DE/translation.json +++ b/web/packages/base/locales/de-DE/translation.json @@ -12,15 +12,15 @@ "enter_email": "E-Mail-Adresse eingeben", "invalid_email_error": "Geben Sie eine gültige E-Mail-Adresse ein", "required": "Erforderlich", - "email_not_registered": "", - "email_already_registered": "", + "email_not_registered": "E-Mail nicht registriert", + "email_already_registered": "E-Mail bereits registriert", "email_sent": "Bestätigungscode an {{email}} gesendet", "check_inbox_hint": "Bitte überprüfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen", "verification_code": "Bestätigungscode", "resend_code": "Code erneut senden", "verify": "Überprüfen", "send_otp": "OTP senden", - "generic_error": "", + "generic_error": "Etwas ist schiefgelaufen", "generic_error_retry": "Ein Fehler ist aufgetreten, bitte versuche es erneut", "invalid_code_error": "Falscher Bestätigungscode", "expired_code_error": "Ihr Bestätigungscode ist abgelaufen", @@ -32,6 +32,7 @@ "set_password": "Passwort setzen", "sign_in": "Einloggen", "incorrect_password": "Falsches Passwort", + "incorrect_password_or_no_account": "Falsches Passwort oder E-Mail nicht registriert", "pick_password_hint": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", "pick_password_caution": "Wir speichern dein Passwort nicht. Wenn du es vergisst, können wir dir nicht helfen, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", "key_generation_in_progress": "Generierung von Verschlüsselungsschlüsseln...", @@ -39,15 +40,16 @@ "referral_source_hint": "Wie hast du von Ente erfahren? (optional)", "referral_source_info": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "password_mismatch_error": "Die Passwörter stimmen nicht überein", + "show_or_hide_password": "Passwort ein- oder ausblenden", "welcome_to_ente_title": "Willkommen bei ", "welcome_to_ente_subtitle": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", "new_album": "Neues Album", "create_albums": "Alben erstellen", - "enter_album_name": "Albumname", + "album_name": "Albumname", "close": "Schließen", - "yes": "", + "yes": "Ja", "no": "Nein", - "nothing_here": "", + "nothing_here": "Hier gibt es noch nichts", "upload": "Hochladen", "import": "Importieren", "add_photos": "Fotos hinzufügen", @@ -56,53 +58,53 @@ "add_photos_count": "{{count, number}} Dateien hinzufügen", "select_photos": "Foto auswählen", "file_upload": "Datei hochladen", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "Wird vorbereitet", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Metadaten werden gelesen", "upload_cancelling": "Verbleibende Uploads werden abgebrochen", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} hochgeladen", + "upload_skipped": "{{count, number}} übersprungen", "initial_load_delay_warning": "Das erste Laden kann einige Zeit in Anspruch nehmen", "no_account": "Kein Konto vorhanden", "existing_account": "Es ist bereits ein Account vorhanden", "create": "Erstellen", - "files_count": "", + "files_count": "{{count, number}} Dateien", "download": "Herunterladen", "download_album": "Album herunterladen", "download_favorites": "Favoriten herunterladen", "download_uncategorized": "Download unkategorisiert", "download_hidden_items": "Versteckte Dateien herunterladen", - "audio": "", - "more": "", - "mouse_scroll": "", - "pan": "", - "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", - "zoom_preset": "", - "toggle_controls": "", - "toggle_live": "", - "toggle_audio": "", - "toggle_favorite": "", - "toggle_archive": "", - "view_info": "", + "audio": "Audio", + "more": "Mehr", + "mouse_scroll": "Mausscrollen", + "pan": "Schwenken", + "pinch": "Kneifen", + "drag": "Ziehen", + "tap_inside_image": "Innerhalb des Bildes tippen", + "tap_outside_image": "Außerhalb des Bildes tippen", + "shortcuts": "Abkürzungen", + "show_shortcuts": "Abkürzungen anzeigen", + "zoom_preset": "Zoom Standard", + "toggle_controls": "Steuerelemente umschalten", + "toggle_live": "Live umschalten", + "toggle_audio": "Audio umschalten", + "toggle_favorite": "Favorit umschalten", + "toggle_archive": "Archiv umschalten", + "view_info": "Information anzeigen", "copy_as_png": "Als PNG kopieren", "toggle_fullscreen": "Vollbild umschalten", - "exit_fullscreen": "", - "go_fullscreen": "", - "zoom": "", - "play": "", - "pause": "", + "exit_fullscreen": "Vollbildmodus verlassen", + "go_fullscreen": "Vollbildmodus öffnen", + "zoom": "Zoom", + "play": "Wiedergeben", + "pause": "Pausieren", "previous": "Vorherige", "next": "Weitere", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Video suchen", + "quality": "Qualität", + "auto": "Automatisch", + "original": "Original", + "speed": "Geschwindigkeit", "title_photos": "Ente Fotos", "title_auth": "Ente Auth", "title_accounts": "Ente Konten", @@ -117,7 +119,7 @@ "selected_count": "{{selected, number}} ausgewählt", "selected_and_yours_count": "{{selected, number}} ausgewählt {{yours, number}} von dir", "delete": "Löschen", - "favorite": "", + "favorite": "Favorit", "convert": "Konvertieren", "multi_folder_upload": "Mehrere Ordner erkannt", "upload_to_choice": "Möchtest du sie hochladen in", @@ -130,7 +132,7 @@ "password_changed_elsewhere": "Passwort anderswo geändert", "password_changed_elsewhere_message": "Bitte melde dich erneut auf diesem Gerät an, um dein neues Passwort zur Authentifizierung zu verwenden.", "go_back": "Zurück", - "account": "", + "account": "Konto", "recovery_key": "Wiederherstellungsschlüssel", "do_this_later": "Auf später verschieben", "save_key": "Schlüssel speichern", @@ -146,9 +148,9 @@ "no_recovery_key_message": "Aufgrund unseres Ende-zu-Ende-Verschlüsselungsprotokolls können Ihre Daten nicht ohne Ihr Passwort oder Ihren Wiederherstellungsschlüssel entschlüsselt werden", "no_two_factor_recovery_key_message": "Bitte sende eine E-Mail an {{emailID}} von deiner registrierten E-Mail-Adresse", "contact_support": "Support kontaktieren", - "help": "", - "ente_help": "", - "blog": "", + "help": "Hilfe", + "ente_help": "Entes Hilfe", + "blog": "Blog", "request_feature": "Feature anfragen", "support": "Support", "cancel": "Abbrechen", @@ -224,10 +226,10 @@ "keep_photos": "Fotos behalten", "share_album": "Album teilen", "sharing_with_self": "Du kannst nicht mit dir selbst teilen", - "sharing_already_shared": "", + "sharing_already_shared": "Sie teilen dies bereits mit {{email}}", "sharing_album_not_allowed": "Albumfreigabe nicht erlaubt", "sharing_disabled_for_free_accounts": "Freigabe ist für kostenlose Konten deaktiviert", - "sharing_user_does_not_exist": "", + "sharing_user_does_not_exist": "Kein Benutzer mit dieser E-Mail gefunden", "search": "Suchen", "search_results": "Ergebnisse durchsuchen", "no_results": "Keine Ergebnisse gefunden", @@ -243,29 +245,29 @@ "terms_and_conditions": "Ich stimme den Bedingungen und Datenschutzrichtlinien zu", "people": "Personen", "indexing_scheduled": "Indizierung ist geplant...", - "indexing_photos": "", - "indexing_fetching": "", - "indexing_people": "", - "syncing_wait": "", - "people_empty_too_few": "", - "unnamed_person": "", - "add_a_name": "", - "new_person": "", - "add_name": "", - "rename_person": "", - "reset_person_confirm": "", - "reset_person_confirm_message": "", - "ignore": "", - "ignore_person_confirm": "", - "ignore_person_confirm_message": "", - "ignored": "", - "show_person": "", - "review_suggestions": "", - "saved_choices": "", - "discard_changes": "", - "discard_changes_confirm_message": "", - "people_suggestions_finding": "", - "people_suggestions_empty": "", + "indexing_photos": "Aktualisiere Indexe...", + "indexing_fetching": "Synchronisiere Indexe...", + "indexing_people": "Synchronisiere Personen...", + "syncing_wait": "Synchronisiere...", + "people_empty_too_few": "Personen werden hier gezeigt, wenn genügend Fotos einer Person existieren", + "unnamed_person": "Unbenannte Person", + "add_a_name": "Name hinzufügen", + "new_person": "Neue Person", + "add_name": "Name hinzufügen", + "rename_person": "Person umbenennen", + "reset_person_confirm": "Person zurücksetzen?", + "reset_person_confirm_message": "Der Name, Gesichtsgruppen und Vorschläge für diese Person werden zurückgesetzt", + "ignore": "Ignorieren", + "ignore_person_confirm": "Person ignorieren?", + "ignore_person_confirm_message": "Diese Gesichtsgruppierung wird nicht in der Personenliste angezeigt werden", + "ignored": "Ignoriert", + "show_person": "Person anzeigen", + "review_suggestions": "Vorschläge überprüfen", + "saved_choices": "Gespeicherte Optionen", + "discard_changes": "Änderungen verwerfen", + "discard_changes_confirm_message": "Sie haben ungespeicherte Änderungen. Diese gehen verloren, wenn Sie schließen ohne zu Speichern", + "people_suggestions_finding": "Ähnliche Gesichter werden gefunden...", + "people_suggestions_empty": "Momentan keine weiteren Vorschläge", "info": "Info", "file_name": "Dateiname", "caption_placeholder": "Eine Beschreibung hinzufügen", @@ -308,7 +310,7 @@ "faq": "FAQ", "takeout_hint": "Entpacke alle Zip-Dateien in den gleichen Ordner und lade das hoch. Oder lade die Zip-Datei direkt hoch. Siehe FAQ für Details.", "destination": "Zielort", - "start": "", + "start": "Start", "last_export_time": "Letztes Exportdatum", "export_again": "Neusynchronisation", "local_storage_not_accessible": "Ihr Browser oder ein Addon blockiert Ente vor der Speicherung von Daten im lokalen Speicher", @@ -325,7 +327,7 @@ "unsupported_files": "Nicht unterstützte Dateien", "unsupported_files_hint": "Ente unterstützt diese Dateiformate noch nicht", "blocked_uploads": "Blockierte Uploads", - "blocked_uploads_hint": "", + "blocked_uploads_hint": "Ihr Browser oder ein Add-on hindert Ente daran, große Dateien mit eTags hochzuladen.", "large_files": "Große Dateien", "large_files_hint": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", "insufficient_storage": "Zu wenig Speicher", @@ -370,16 +372,16 @@ "confirm_remove_incl_others_message": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", "oldest": "Ältestem", "last_updated": "Zuletzt aktualisiert", - "name": "", + "name": "Name", "fix_creation_time": "Zeit reparieren", "fix_creation_time_in_progress": "Zeit wird repariert", "fix_creation_time_file_updated": "Datei-Zeit aktualisiert", "fix_creation_time_completed": "Alle Dateien erfolgreich aktualisiert", "fix_creation_time_completed_with_errors": "Aktualisierung der Dateizeit für einige Dateien fehlgeschlagen, bitte versuche es erneut", "fix_creation_time_options": "Wählen Sie die Option, die Sie verwenden möchten", - "exif_date_time_original": "", - "exif_date_time_digitized": "", - "exif_metadata_date": "", + "exif_date_time_original": "Exif:DateTimeOriginal", + "exif_date_time_digitized": "Exif:DateTimeDigitized", + "exif_metadata_date": "Exif:MetadataDate", "custom_time": "Benutzerdefinierte Zeit", "caption_character_limit": "Maximal 5000 Zeichen", "sharing_details": "Details teilen", @@ -412,9 +414,9 @@ "or_add_existing": "Oder eine Vorherige auswählen", "not_found": "404 - Nicht gefunden", "link_expired": "Link ist abgelaufen", - "link_expired_message": "", + "link_expired_message": "Dieser Link ist entweder abgelaufen oder wurde deaktiviert", "manage_link": "Link verwalten", - "link_request_limit_exceeded": "", + "link_request_limit_exceeded": "Dieses Album wurde auf zu vielen Geräten angesehen", "allow_downloads": "Downloads erlauben", "allow_adding_photos": "Hinzufügen von Fotos erlauben", "allow_adding_photos_hint": "Erlaube Personen mit diesem Link, Fotos zum gemeinsamen Album hinzuzufügen.", @@ -449,13 +451,13 @@ "folder": "Ordner", "google_takeout": "Google Takeout", "deduplicate_files": "Duplikate bereinigen", - "remove_duplicates": "", - "total_size": "", - "count": "", - "deselect_all": "", - "no_duplicates": "", - "duplicate_group_description": "", - "remove_duplicates_button_count": "", + "remove_duplicates": "Duplikate entfernen", + "total_size": "Gesamtgröße", + "count": "Anzahl", + "deselect_all": "Auswahl aufheben", + "no_duplicates": "Keine Duplikate", + "duplicate_group_description": "{{count}} Elemente, {{itemSize}} pro", + "remove_duplicates_button_count": "Löschen Sie {{count, number}} Elemente", "stop_uploads_title": "Hochladen stoppen?", "stop_uploads_message": "Bist du sicher, dass du alle laufenden Uploads abbrechen möchtest?", "yes_stop_uploads": "Ja, Hochladen stoppen", @@ -492,8 +494,8 @@ "stop_watching_folder_message": "Deine bestehenden Dateien werden nicht gelöscht, aber das verknüpfte Ente-Album wird bei Änderungen in diesem Ordner nicht mehr aktualisiert.", "yes_stop": "Ja, Stopp", "change_folder": "Ordner ändern", - "view_logs": "", - "view_logs_message": "", + "view_logs": "Protokolle anzeigen", + "view_logs_message": "

Dies wird Debug-Protokolle anzeigen, welche Sie uns per E-Mail zusenden können, um ihr Problem zu analysieren.

Beachten Sie, dass Dateinamen enthalten sind um Probleme mit spezifischen Dateien nachvollziehen zu können.

", "weak_device_hint": "Dein Browser ist nicht leistungsstark genug, um deine Bilder zu verschlüsseln. Versuche, dich an einem Computer bei Ente anzumelden, oder lade dir die Ente-App für dein Gerät (Handy oder Desktop) herunter.", "drag_and_drop_hint": "Oder ziehe Dateien per Drag-and-Drop in das Ente-Fenster", "authenticate": "Authentifizieren", @@ -529,9 +531,9 @@ "ml_consent_description": "

Wenn du das maschinelle Lernen aktivierst, extrahiert Ente Informationen wie die Gesichtsgeometrie aus Dateien, auch aus denen, die mit dir geteilt wurden.

Dies geschieht auf deinem Gerät, und alle generierten biometrischen Informationen werden Ende-zu-Ende verschlüsselt.

Bitte klicke hier für weitere Details zu dieser Funktion in unserer Datenschutzerklärung

", "ml_consent_confirmation": "Ich verstehe und möchte maschinelles Lernen aktivieren", "labs": "Experimente", - "passphrase_strength_weak": "Passwortstärke: Schwach", - "passphrase_strength_moderate": "Passwortstärke: Moderat", - "passphrase_strength_strong": "Passwortstärke: Stark", + "password_strength_weak": "Passwortstärke: Schwach", + "password_strength_moderate": "Passwortstärke: Moderat", + "password_strength_strong": "Passwortstärke: Stark", "preferences": "Einstellungen", "language": "Sprache", "advanced": "Erweitert", @@ -544,7 +546,7 @@ "gb": "GB", "tb": "TB" }, - "stop": "", + "stop": "Stopp", "sync_continuously": "Stets aktuell halten", "export_starting": "Starte Export...", "export_preparing": "Vorbereiten...", @@ -572,7 +574,7 @@ "at": "um", "auth_next": "Weiter", "auth_download_mobile_app": "Lade unsere smartphone App herunter, um deine Schlüssel zu verwalten", - "no_codes_added_yet": "", + "no_codes_added_yet": "Noch keine Codes hinzugefügt", "hide": "Ausblenden", "unhide": "Einblenden", "sort_by": "Sortieren nach", @@ -592,8 +594,8 @@ "image": "Bild", "video": "Video", "live_photo": "Live-foto", - "live": "", - "edit_image": "", + "live": "Live", + "edit_image": "Bild bearbeiten", "photo_editor": "Foto-editor", "confirm_editor_close": "Editor wirklich schließen?", "confirm_editor_close_message": "Lade dein bearbeitetes Bild herunter oder speichere es in Ente, um die Änderungen nicht zu verlieren.", @@ -622,10 +624,11 @@ "reset": "Zurücksetzen", "faster_upload": "Schnelleres Hochladen", "faster_upload_description": "Uploads über nahegelegene Server leiten", - "open_ente_on_startup": "", + "open_ente_on_startup": "Ente beim PC-Start ausführen", "cast_album_to_tv": "Album auf Fernseher wiedergeben", + "cast_to_tv": "", "enter_cast_pin_code": "Gib den Code auf dem Fernseher unten ein, um dieses Gerät zu koppeln.", - "code": "", + "code": "Code", "pair_device_to_tv": "Geräte koppeln", "tv_not_found": "Fernseher nicht gefunden. Hast du die PIN korrekt eingegeben?", "cast_auto_pair": "Automatisch verbinden", @@ -656,7 +659,7 @@ "check_status": "Status überprüfen", "passkey_login_instructions": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.", "passkey_login": "Mit Passkey anmelden", - "totp_login": "", + "totp_login": "Mit TOTP anmelden", "passkey": "Passkey", "passkey_verify_description": "Überprüfe deinen Passkey, um dich in dein Konto einzuloggen.", "waiting_for_verification": "Warte auf Bestätigung...", @@ -671,8 +674,14 @@ "server_endpoint": "Server Endpunkt", "more_information": "Weitere Informationen", "save": "Speichern", - "theme": "", - "system": "", - "light": "", - "dark": "" + "theme": "Stil", + "system": "System", + "light": "Hell", + "dark": "Dunkel", + "streamable_videos": "Streambare Videos", + "processing_videos_status": "Verarbeite Videos...", + "share_favorites": "Favoriten teilen", + "person_favorites": "{{name}}s Favoriten", + "shared_favorites": "Geteilte Favoriten", + "added_by_name": "Von {{name}} hinzugefügt" } diff --git a/web/packages/base/locales/el-GR/translation.json b/web/packages/base/locales/el-GR/translation.json index c793a977b7..84d608d14a 100644 --- a/web/packages/base/locales/el-GR/translation.json +++ b/web/packages/base/locales/el-GR/translation.json @@ -32,6 +32,7 @@ "set_password": "Ορισμός κωδικού πρόσβασης", "sign_in": "Σύνδεση", "incorrect_password": "Λάθος κωδικός πρόσβασης", + "incorrect_password_or_no_account": "", "pick_password_hint": "Εισάγετε έναν κωδικό πρόσβασης που μπορούμε να χρησιμοποιήσουμε για την κρυπτογράφηση των δεδομένων σας", "pick_password_caution": "Δεν αποθηκεύουμε τον κωδικό πρόσβασής σας, επομένως, αν τον ξεχάσετε, δεν θα μπορέσουμε να σας βοηθήσουμε να ανακτήσετε τα δεδομένα σας χωρίς κλειδί ανάκτησης.", "key_generation_in_progress": "Δημιουργία κλειδιών κρυπτογράφησης...", @@ -39,11 +40,12 @@ "referral_source_hint": "Πώς ακούσατε για το Ente; (προαιρετικό)", "referral_source_info": "Δεν παρακολουθούμε τις εγκαταστάσεις εφαρμογών. Θα μας βοηθούσε αν μας λέγατε που μας βρήκατε!", "password_mismatch_error": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", + "show_or_hide_password": "", "welcome_to_ente_title": "Καλώς ήρθατε στο ", "welcome_to_ente_subtitle": "", "new_album": "Νέο άλμπουμ", "create_albums": "", - "enter_album_name": "Όνομα άλμπουμ", + "album_name": "Όνομα άλμπουμ", "close": "Κλείσιμο", "yes": "", "no": "Όχι", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "Εργαστήρια", - "passphrase_strength_weak": "Ισχύς κωδικού πρόσβασης: Ασθενής", - "passphrase_strength_moderate": "Ισχύς κωδικού πρόσβασης: Μέτριος", - "passphrase_strength_strong": "Ισχύς κωδικού πρόσβασης: Ισχυρός", + "password_strength_weak": "Ισχύς κωδικού πρόσβασης: Ασθενής", + "password_strength_moderate": "Ισχύς κωδικού πρόσβασης: Μέτριος", + "password_strength_strong": "Ισχύς κωδικού πρόσβασης: Ισχυρός", "preferences": "Προτιμήσεις", "language": "Γλώσσα", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Αναπαραγωγή άλμπουμ στην τηλεόραση", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "Ζεύξη συσκευών", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/en-US/translation.json b/web/packages/base/locales/en-US/translation.json index 9f75e19217..0d6579b8c2 100644 --- a/web/packages/base/locales/en-US/translation.json +++ b/web/packages/base/locales/en-US/translation.json @@ -32,18 +32,20 @@ "set_password": "Set password", "sign_in": "Sign in", "incorrect_password": "Incorrect password", + "incorrect_password_or_no_account": "Incorrect password or email not registered", "pick_password_hint": "Please enter a password that we can use to encrypt your data", "pick_password_caution": "We don't store your password, so if you forget it, we will not be able to help you recover your data without a recovery key.", "key_generation_in_progress": "Generating encryption keys...", "confirm_password": "Confirm password", "referral_source_hint": "How did you hear about Ente? (optional)", - "referral_source_info": "We don't track app installs, It'd help us if you told us where you found us!", + "referral_source_info": "We don't track app installs, it'd help us if you told us where you found us!", "password_mismatch_error": "Passwords don't match", + "show_or_hide_password": "Show or hide password", "welcome_to_ente_title": "Welcome to ", "welcome_to_ente_subtitle": "End to end encrypted photo storage and sharing", "new_album": "New album", "create_albums": "Create albums", - "enter_album_name": "Album name", + "album_name": "Album name", "close": "Close", "yes": "Yes", "no": "No", @@ -529,9 +531,9 @@ "ml_consent_description": "

If you enable machine learning, Ente will extract information like face geometry from files, including those shared with you.

This will happen on your device, and any generated biometric information will be end-to-end encrypted.

Please click here for more details about this feature in our privacy policy

", "ml_consent_confirmation": "I understand, and wish to enable machine learning", "labs": "Labs", - "passphrase_strength_weak": "Password strength: Weak", - "passphrase_strength_moderate": "Password strength: Moderate", - "passphrase_strength_strong": "Password strength: Strong", + "password_strength_weak": "Password strength: Weak", + "password_strength_moderate": "Password strength: Moderate", + "password_strength_strong": "Password strength: Strong", "preferences": "Preferences", "language": "Language", "advanced": "Advanced", @@ -624,6 +626,7 @@ "faster_upload_description": "Route uploads through nearby servers", "open_ente_on_startup": "Open Ente on startup", "cast_album_to_tv": "Play album on TV", + "cast_to_tv": "Play on TV", "enter_cast_pin_code": "Enter the code you see on the TV below to pair this device.", "code": "Code", "pair_device_to_tv": "Pair devices", @@ -674,5 +677,11 @@ "theme": "Theme", "system": "System", "light": "Light", - "dark": "Dark" + "dark": "Dark", + "streamable_videos": "Streamable videos", + "processing_videos_status": "Processing videos...", + "share_favorites": "Share favorites", + "person_favorites": "{{name}}'s favorites", + "shared_favorites": "Shared favorites", + "added_by_name": "Added by {{name}}" } diff --git a/web/packages/base/locales/es-ES/translation.json b/web/packages/base/locales/es-ES/translation.json index 9573a40300..7373ee27f2 100644 --- a/web/packages/base/locales/es-ES/translation.json +++ b/web/packages/base/locales/es-ES/translation.json @@ -32,6 +32,7 @@ "set_password": "Definir contraseña", "sign_in": "Ingresar", "incorrect_password": "Contraseña incorrecta", + "incorrect_password_or_no_account": "", "pick_password_hint": "Introducir una contraseña que podamos usar para cifrar sus datos", "pick_password_caution": "No guardamos su contraseña, así que si la olvida, no podremos ayudarte a recuperar tus datos sin una clave de recuperación.", "key_generation_in_progress": "Generando claves de encriptación...", @@ -39,11 +40,12 @@ "referral_source_hint": "¿Cómo conociste Ente? (opcional)", "referral_source_info": "No rastreamos la instalación de las aplicaciones. ¡Nos ayudaría si nos dijera dónde nos encontró!", "password_mismatch_error": "Las contraseñas no coinciden", + "show_or_hide_password": "", "welcome_to_ente_title": "Bienvenido a ", "welcome_to_ente_subtitle": "Almacenamiento y compartición de fotos cifradas de extremo a extremo", "new_album": "Nuevo álbum", "create_albums": "Crear álbumes", - "enter_album_name": "Nombre del álbum", + "album_name": "Nombre del álbum", "close": "Cerrar", "yes": "Sí", "no": "No", @@ -56,12 +58,12 @@ "add_photos_count": "Añadir {{count, number}} fotos", "select_photos": "Seleccionar fotos", "file_upload": "Subir archivo", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "Preparando", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Leyendo archivos de metadatos", "upload_cancelling": "Cancelar subidas restantes", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} subido", + "upload_skipped": "{{count, number}} omitido", "initial_load_delay_warning": "La primera carga puede tomar algún tiempo", "no_account": "No tienes una cuenta", "existing_account": "Ya tienes una cuenta", @@ -72,37 +74,37 @@ "download_favorites": "Descargar favoritos", "download_uncategorized": "Descargar no categorizados", "download_hidden_items": "Descargar elementos ocultos", - "audio": "", - "more": "", - "mouse_scroll": "", - "pan": "", - "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", - "zoom_preset": "", - "toggle_controls": "", - "toggle_live": "", - "toggle_audio": "", - "toggle_favorite": "", - "toggle_archive": "", - "view_info": "", + "audio": "Audio", + "more": "Más", + "mouse_scroll": "Desplazamiento del ratón", + "pan": "Desplazar", + "pinch": "Pellizcar", + "drag": "Arrastrar", + "tap_inside_image": "Toca dentro de la imagen", + "tap_outside_image": "Toca fuera de la imagen", + "shortcuts": "Accesos directos", + "show_shortcuts": "Mostrar accesos directos", + "zoom_preset": "Zoom preestablecido", + "toggle_controls": "Alternar controles", + "toggle_live": "Alternar en directo", + "toggle_audio": "Alternar audio", + "toggle_favorite": "Alternar favoritos", + "toggle_archive": "Alternar archivo", + "view_info": "Ver información", "copy_as_png": "Copiar como PNG", "toggle_fullscreen": "Alternar pantalla completa", - "exit_fullscreen": "", - "go_fullscreen": "", - "zoom": "", - "play": "", - "pause": "", + "exit_fullscreen": "Salir de la pantalla completa", + "go_fullscreen": "Ir a pantalla completa", + "zoom": "Ampliar", + "play": "Reproducir", + "pause": "Pausar", "previous": "Anterior", "next": "Siguiente", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Buscar vídeo", + "quality": "Calidad", + "auto": "Auto", + "original": "Original", + "speed": "Velocidad", "title_photos": "ente Fotos", "title_auth": "ente Auth", "title_accounts": "Cuentas de Ente", @@ -478,7 +480,7 @@ "used": "usado", "you": "Usted", "family": "Familia", - "free": "gratis", + "free": "disponible", "of": "de", "watch_folders": "Ver carpetas", "watched_folders": "Ver carpetas", @@ -529,9 +531,9 @@ "ml_consent_description": "

Si activas el aprendizaje automático, Ente extraerá información como la geometría de la cara de los archivos, incluyendo aquellos compartidos contigo.

Esto sucederá en tu dispositivo, y cualquier información biométrica generada será cifrada de extremo a extremo.

Por favor haz click aqui para mas detalles acerta de esta característica en nuestra política de privacidad

", "ml_consent_confirmation": "Entiendo y deseo habilitar el aprendizaje automático", "labs": "Laboratorios", - "passphrase_strength_weak": "Fortaleza de la contraseña: débil", - "passphrase_strength_moderate": "Fortaleza de contraseña: moderada", - "passphrase_strength_strong": "Fortaleza de contraseña: fuerte", + "password_strength_weak": "Fortaleza de la contraseña: débil", + "password_strength_moderate": "Fortaleza de contraseña: moderada", + "password_strength_strong": "Fortaleza de contraseña: fuerte", "preferences": "Preferencias", "language": "Idioma", "advanced": "Avanzado", @@ -592,8 +594,8 @@ "image": "Imagen", "video": "Video", "live_photo": "Foto en vivo", - "live": "", - "edit_image": "", + "live": "Directo", + "edit_image": "Editar imagen", "photo_editor": "Editor de fotos", "confirm_editor_close": "¿Estás seguro de que quieres cerrar el editor?", "confirm_editor_close_message": "Descargue su imagen editada o guarde una copia en Ente para mantener sus cambios.", @@ -624,6 +626,7 @@ "faster_upload_description": "Enrutar subidas a través de servidores cercanos", "open_ente_on_startup": "Abrir ente al iniciar", "cast_album_to_tv": "Reproducir álbum en TV", + "cast_to_tv": "", "enter_cast_pin_code": "Introduce el código que ves en el televisor para emparejar este dispositivo.", "code": "Código", "pair_device_to_tv": "Emparejar dispositivos", @@ -674,5 +677,11 @@ "theme": "Tema", "system": "Sistema", "light": "Claro", - "dark": "Oscuro" + "dark": "Oscuro", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/et-EE/translation.json b/web/packages/base/locales/et-EE/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/et-EE/translation.json +++ b/web/packages/base/locales/et-EE/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fa-IR/translation.json b/web/packages/base/locales/fa-IR/translation.json index 6a2869bc1d..c6de604eac 100644 --- a/web/packages/base/locales/fa-IR/translation.json +++ b/web/packages/base/locales/fa-IR/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "به خوش آمدید", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fi-FI/translation.json b/web/packages/base/locales/fi-FI/translation.json index 6842f88946..a1cfb43200 100644 --- a/web/packages/base/locales/fi-FI/translation.json +++ b/web/packages/base/locales/fi-FI/translation.json @@ -32,6 +32,7 @@ "set_password": "Aseta salasana", "sign_in": "Kirjaudu sisään", "incorrect_password": "Väärä salasana", + "incorrect_password_or_no_account": "", "pick_password_hint": "Anna salasana, jota voimme käyttää salaamaan tietosi", "pick_password_caution": "Emme säilytä salasanaasi, joten jos unohdat sen, emme voi auttaa sinua palauttamaan tietojasi ilman palautusavainta.", "key_generation_in_progress": "Luodaan salausavaimia...", @@ -39,11 +40,12 @@ "referral_source_hint": "Miten kuulit Entestä? (valinnainen)", "referral_source_info": "Emme seuraa sovelluksen asennuksia. Se auttaisi meitä, jos kertoisit mistä löysit meidät!", "password_mismatch_error": "Salasanat eivät täsmää", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "Päästä päähän salattu valokuvien tallennustila ja jakaminen", "new_album": "Uusi albumi", "create_albums": "Luo albumit", - "enter_album_name": "Albumin nimi", + "album_name": "Albumin nimi", "close": "Sulje", "yes": "Kyllä", "no": "Ei", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/fr-FR/translation.json b/web/packages/base/locales/fr-FR/translation.json index 629a8c6993..dda18157e7 100644 --- a/web/packages/base/locales/fr-FR/translation.json +++ b/web/packages/base/locales/fr-FR/translation.json @@ -32,6 +32,7 @@ "set_password": "Définir le mot de passe", "sign_in": "Connexion", "incorrect_password": "Mot de passe non valide", + "incorrect_password_or_no_account": "Mot de passe incorrect ou adresse email non enregistrée", "pick_password_hint": "Veuillez saisir un mot de passe que nous pourrons utiliser pour chiffrer vos données", "pick_password_caution": "Nous ne stockons pas votre mot de passe, donc si vous le perdez, nous ne pourrons pas vous aider à récupérer vos données sans une clé de récupération.", "key_generation_in_progress": "Génération des clés de chiffrement...", @@ -39,11 +40,12 @@ "referral_source_hint": "Comment avez-vous entendu parler de Ente? (facultatif)", "referral_source_info": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", "password_mismatch_error": "Les mots de passe ne correspondent pas", + "show_or_hide_password": "Afficher/Masquer le mot de passe", "welcome_to_ente_title": "Bienvenue sur ", "welcome_to_ente_subtitle": "Stockage et partage de photos chiffrés de bout en bout", "new_album": "Nouvel album", "create_albums": "Créer des albums", - "enter_album_name": "Nom de l'album", + "album_name": "Nom de l'album", "close": "Fermer", "yes": "Oui", "no": "Non", @@ -56,12 +58,12 @@ "add_photos_count": "Ajouter {{count, number}} éléments", "select_photos": "Sélectionner des photos", "file_upload": "Fichier chargé", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "En préparation", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Lecture des fichiers de métadonnées", "upload_cancelling": "Annulation des chargements restants", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} chargé", + "upload_skipped": "{{count, number}} ignoré(s)", "initial_load_delay_warning": "Le premier affichage peut prendre du temps", "no_account": "Je n'ai pas de compte", "existing_account": "J'ai déjà un compte", @@ -94,19 +96,19 @@ "exit_fullscreen": "Quitter le mode plein écran", "go_fullscreen": "Passer en plein écran", "zoom": "Zoom", - "play": "", - "pause": "", + "play": "Lecture", + "pause": "Pause", "previous": "Précédent", "next": "Suivant", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Recherche vidéo", + "quality": "Qualité", + "auto": "Auto", + "original": "Original", + "speed": "Vitesse", "title_photos": "Ente Photos", "title_auth": "Ente Auth", "title_accounts": "Comptes Ente", - "upload_first_photo": "Chargez votre 1ere photo", + "upload_first_photo": "Chargez votre 1ère photo", "import_your_folders": "Importez vos dossiers", "upload_dropzone_hint": "Déposez pour sauvegarder vos fichiers", "watch_folder_dropzone_hint": "Déposez pour ajouter un dossier surveillé", @@ -478,7 +480,7 @@ "used": "utilisé", "you": "Vous", "family": "Famille", - "free": "gratuit", + "free": "libre", "of": "de", "watch_folders": "Surveiller des dossiers", "watched_folders": "Dossiers surveillés", @@ -529,9 +531,9 @@ "ml_consent_description": "

Si vous activez l'apprentissage automatique, Ente extraira de vos photos et de celles partagées avec vous des informations comme la géométrie des visages.

Cela se fera sur votre appareil, et toutes les données biométriques générées seront chiffrées de bout-en-bout.

Veuillez cliquer ici pour accéder à notre politique de confidentialité, vous y trouverez plus de détails concernant cette fonction

", "ml_consent_confirmation": "Je comprends, et souhaite activer l'apprentissage automatique", "labs": "Labo", - "passphrase_strength_weak": "Sécurité du mot de passe : faible", - "passphrase_strength_moderate": "Sécurité du mot de passe : moyenne", - "passphrase_strength_strong": "Sécurité du mot de passe : forte", + "password_strength_weak": "Sécurité du mot de passe : faible", + "password_strength_moderate": "Sécurité du mot de passe : moyenne", + "password_strength_strong": "Sécurité du mot de passe : forte", "preferences": "Préférences", "language": "Langue", "advanced": "Avancé", @@ -624,6 +626,7 @@ "faster_upload_description": "Router les chargements vers les serveurs à proximité", "open_ente_on_startup": "Ouvrir Ente au démarrage", "cast_album_to_tv": "Jouer l'album sur la TV", + "cast_to_tv": "Afficher sur la TV", "enter_cast_pin_code": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.", "code": "Code", "pair_device_to_tv": "Associer les appareils", @@ -643,7 +646,7 @@ "rename_passkey": "Renommer le code d'accès", "add_passkey": "Ajouter un code d'accès", "enter_passkey_name": "Entrez le nom du code d'accès", - "passkeys_description": "Les codes d'ccès sont un deuxième facteur moderne et sécurisé pour votre compte Ente. Ils utilisent l'authentification biométrique de l'appareil pour des raisons de commodité et de sécurité.", + "passkeys_description": "Les codes d'accès sont un deuxième facteur moderne et sécurisé pour votre compte Ente. Ils utilisent l'authentification biométrique de l'appareil pour des raisons de commodité et de sécurité.", "created_at": "Créé le", "passkey_add_failed": "Impossible d'ajouter une clé d'accès", "passkey_login_failed": "Échec de la connexion via code d'accès", @@ -674,5 +677,11 @@ "theme": "Thème", "system": "Système", "light": "Clair", - "dark": "Sombre" + "dark": "Sombre", + "streamable_videos": "Vidéos diffusables", + "processing_videos_status": "Traitement des vidéos...", + "share_favorites": "Partager les favoris", + "person_favorites": "Favoris de {{name}}", + "shared_favorites": "Favoris partagés", + "added_by_name": "Ajouté par {{name}}" } diff --git a/web/packages/base/locales/gu-IN/translation.json b/web/packages/base/locales/gu-IN/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/gu-IN/translation.json +++ b/web/packages/base/locales/gu-IN/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/hi-IN/translation.json b/web/packages/base/locales/hi-IN/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/hi-IN/translation.json +++ b/web/packages/base/locales/hi-IN/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/hu-HU/translation.json b/web/packages/base/locales/hu-HU/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/hu-HU/translation.json +++ b/web/packages/base/locales/hu-HU/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/id-ID/translation.json b/web/packages/base/locales/id-ID/translation.json index aa44765052..f1ba6d649f 100644 --- a/web/packages/base/locales/id-ID/translation.json +++ b/web/packages/base/locales/id-ID/translation.json @@ -32,18 +32,20 @@ "set_password": "Atur sandi", "sign_in": "Masuk", "incorrect_password": "Sandi salah", + "incorrect_password_or_no_account": "", "pick_password_hint": "Silakan masukkan kata sandi yang dapat kami gunakan untuk mengenkripsi data Anda", "pick_password_caution": "Kami tidak menyimpan sandi kamu, jadi jika kamu lupa, kami tidak akan dapat membantumu memulihkan data tanpa kunci pemulihan.", "key_generation_in_progress": "Menghasilkan kunci enkripsi...", "confirm_password": "Konfirmasi sandi", "referral_source_hint": "Dari mana Anda menemukan Ente? (opsional)", - "referral_source_info": "Kami tidak melacak pemasangan aplikasi, Ini akan membantu kami jika Anda memberi tahu kami di mana Anda menemukan kami!", + "referral_source_info": "Kami tidak melacak pemasangan aplikasi, ini akan membantu kami jika Anda memberi tahu kami di mana Anda menemukan kami!", "password_mismatch_error": "Sandi tidak cocok", + "show_or_hide_password": "", "welcome_to_ente_title": "Selamat datang di ", "welcome_to_ente_subtitle": "Penyimpanan dan berbagi foto terenkripsi ujung ke ujung", "new_album": "Album baru", "create_albums": "", - "enter_album_name": "Nama album", + "album_name": "Nama album", "close": "Tutup", "yes": "", "no": "Tidak", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "Keamanan sandi: Lemah", - "passphrase_strength_moderate": "Keamanan sandi: Sedang", - "passphrase_strength_strong": "Keamanan sandi: Kuat", + "password_strength_weak": "Keamanan sandi: Lemah", + "password_strength_moderate": "Keamanan sandi: Sedang", + "password_strength_strong": "Keamanan sandi: Kuat", "preferences": "Preferensi", "language": "Bahasa", "advanced": "Lanjutan", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Putar album di TV", + "cast_to_tv": "", "enter_cast_pin_code": "Masukkan kode yang ditampilkan TV di bawah untuk menautkan perangkat ini.", "code": "", "pair_device_to_tv": "Tautkan perangkat", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/is-IS/translation.json b/web/packages/base/locales/is-IS/translation.json index 021f858680..4432990468 100644 --- a/web/packages/base/locales/is-IS/translation.json +++ b/web/packages/base/locales/is-IS/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "Rangt lykilorð", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "Loka", "yes": "", "no": "Nei", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/it-IT/translation.json b/web/packages/base/locales/it-IT/translation.json index 721ec04dfd..494362d55f 100644 --- a/web/packages/base/locales/it-IT/translation.json +++ b/web/packages/base/locales/it-IT/translation.json @@ -32,6 +32,7 @@ "set_password": "Imposta una password", "sign_in": "Accedi", "incorrect_password": "Password sbagliata", + "incorrect_password_or_no_account": "", "pick_password_hint": "Inserisci una password per crittografare i tuoi dati", "pick_password_caution": "Non memorizziamo la tua password, quindi se la dimentichi, non saremo in grado di aiutarti a recuperare i tuoi dati senza una chiave di recupero.", "key_generation_in_progress": "Generazione delle chiavi di crittografia...", @@ -39,11 +40,12 @@ "referral_source_hint": "Come hai conosciuto Ente? (opzionale)", "referral_source_info": "Non teniamo traccia delle installazioni dell'app. Ci sarebbe utile se ci dicessi come ci hai trovato!", "password_mismatch_error": "Le password non corrispondono", + "show_or_hide_password": "", "welcome_to_ente_title": "Benvenuto su ", "welcome_to_ente_subtitle": "Archiviazione e condivisione di foto crittografate end-to-end", "new_album": "Nuovo album", "create_albums": "Crea album", - "enter_album_name": "Nome album", + "album_name": "Nome album", "close": "Chiudi", "yes": "", "no": "No", @@ -76,8 +78,8 @@ "more": "", "mouse_scroll": "", "pan": "", - "pinch": "", - "drag": "", + "pinch": "Pizzica", + "drag": "Trascina", "tap_inside_image": "", "tap_outside_image": "", "shortcuts": "", @@ -529,9 +531,9 @@ "ml_consent_description": "

Se abiliti l'apprendimento automatico, Ente estrarrà informazioni come la geometria del volto dai file, inclusi quelli condivisi con te.

Questo sarà fatto sul tuo dispositivo e qualsiasi informazione biometrica generata sarà crittografata end-to-end.

Clicca qui per maggiori dettagli su questa funzione nella nostra informativa sulla privacy

", "ml_consent_confirmation": "Comprendo e desidero abilitare l'apprendimento automatico", "labs": "Laboratori", - "passphrase_strength_weak": "Sicurezza password: Debole", - "passphrase_strength_moderate": "Sicurezza password: Moderata", - "passphrase_strength_strong": "Sicurezza password: Forte", + "password_strength_weak": "Sicurezza password: Debole", + "password_strength_moderate": "Sicurezza password: Moderata", + "password_strength_strong": "Sicurezza password: Forte", "preferences": "Preferenze", "language": "Lingua", "advanced": "Avanzate", @@ -624,6 +626,7 @@ "faster_upload_description": "Effettua l'upload attraverso i server vicini", "open_ente_on_startup": "", "cast_album_to_tv": "Riproduci album sulla TV", + "cast_to_tv": "", "enter_cast_pin_code": "Inserisci il codice che vedi sulla TV qui sotto per associare questo dispositivo.", "code": "", "pair_device_to_tv": "Associa dispositivi", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ja-JP/translation.json b/web/packages/base/locales/ja-JP/translation.json index c19c0b589e..97cccea680 100644 --- a/web/packages/base/locales/ja-JP/translation.json +++ b/web/packages/base/locales/ja-JP/translation.json @@ -32,6 +32,7 @@ "set_password": "パスワードを設定", "sign_in": "サインイン", "incorrect_password": "パスワードが間違っています", + "incorrect_password_or_no_account": "", "pick_password_hint": "あなたのデータを暗号化するためのパスワードを入力してください", "pick_password_caution": "私たちはあなたのパスワードを保存していないため、もし忘れてしまった場合、リカバリーキーなしではデータを回復するお手伝いができません。", "key_generation_in_progress": "暗号化キーを生成中...", @@ -39,11 +40,12 @@ "referral_source_hint": "Enteをどのように知りましたか?(任意)", "referral_source_info": "アプリのインストールを追跡していませんが、どこで私たちを見つけたか教えていただけると助かります!", "password_mismatch_error": "パスワードが一致しません", + "show_or_hide_password": "", "welcome_to_ente_title": " へようこそ", "welcome_to_ente_subtitle": "エンドツーエンド暗号化された写真の保存と共有", "new_album": "新しいアルバム", "create_albums": "アルバムを作成", - "enter_album_name": "アルバムの名前", + "album_name": "アルバムの名前", "close": "閉じる", "yes": "はい", "no": "いいえ", @@ -529,9 +531,9 @@ "ml_consent_description": "

機械学習を有効にすると、Ente はファイル(共有されたものを含む)から顔の形状などの情報を抽出します。

これはお使いのデバイス上で行われ、生成された生体情報はエンドツーエンドで暗号化されます。

この機能の詳細については、プライバシーポリシーをご覧ください

", "ml_consent_confirmation": "理解して、機械学習を有効にします", "labs": "Labs", - "passphrase_strength_weak": "パスワード強度: 弱い", - "passphrase_strength_moderate": "パスワード強度: 普通", - "passphrase_strength_strong": "パスワード強度: 強い", + "password_strength_weak": "パスワード強度: 弱い", + "password_strength_moderate": "パスワード強度: 普通", + "password_strength_strong": "パスワード強度: 強い", "preferences": "設定", "language": "言語", "advanced": "高度な設定", @@ -624,6 +626,7 @@ "faster_upload_description": "近くのサーバー経由でのアップロード", "open_ente_on_startup": "開始時に Ente を開く", "cast_album_to_tv": "TVでアルバムを再生", + "cast_to_tv": "", "enter_cast_pin_code": "このデバイスをペアリングするには、以下のテレビで表示されているコードを入力してください。", "code": "コード", "pair_device_to_tv": "デバイスを接続する", @@ -674,5 +677,11 @@ "theme": "テーマ", "system": "システム", "light": "ライト", - "dark": "ダーク" + "dark": "ダーク", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/km-KH/translation.json b/web/packages/base/locales/km-KH/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/km-KH/translation.json +++ b/web/packages/base/locales/km-KH/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ko-KR/translation.json b/web/packages/base/locales/ko-KR/translation.json index 07f68062db..89994745ad 100644 --- a/web/packages/base/locales/ko-KR/translation.json +++ b/web/packages/base/locales/ko-KR/translation.json @@ -1,76 +1,78 @@ { "intro_slide_1_title": "당신의 추억을 위한
비공개 백업", - "intro_slide_1": "종단간 암호화를 기본적으로 지원합니다", - "intro_slide_2_title": "낙진 대피소에
안전하게 보관됨", - "intro_slide_2": "장기 보존을 위해 설계되었습니다", + "intro_slide_1": "종단간 암호화", + "intro_slide_2_title": "방공호에서
안전하게 보관", + "intro_slide_2": "장기 보존을 위한 설계", "intro_slide_3_title": "모든 기기에서
사용 가능", "intro_slide_3": "안드로이드, iOS, 웹, 데스크탑", "login": "로그인", "sign_up": "회원가입", - "new_to_ente": "Ente 의 새소식", - "existing_user": "기존 회원 로그인", + "new_to_ente": "Ente를 처음 사용합니다", + "existing_user": "기존 회원입니다", "enter_email": "이메일 주소를 입력하세요", "invalid_email_error": "올바른 이메일을 입력하세요", "required": "필수", - "email_not_registered": "", - "email_already_registered": "", - "email_sent": "{{email}} 로 인증 코드가 전송되었습니다", - "check_inbox_hint": "인증을 완료하기 위해 당신의 메일 수신함(그리고 스팸 수신함)을 확인하세요.", + "email_not_registered": "이메일이 등록되지 않았습니다", + "email_already_registered": "이미 등록된 이메일입니다", + "email_sent": "{{email}}로 인증 코드가 전송되었습니다", + "check_inbox_hint": "인증을 완료하기 위해 메일 수신함(그리고 스팸 수신함)을 확인하세요", "verification_code": "인증 코드", - "resend_code": "코드 재전송하기", + "resend_code": "코드 재전송", "verify": "인증", - "send_otp": "", - "generic_error": "", - "generic_error_retry": "문제가 생긴 것 같아요. 다시 시도하세요", + "send_otp": "인증 코드 전송", + "generic_error": "문제가 발생했습니다", + "generic_error_retry": "문제가 발생했습니다, 다시 시도해주세요", "invalid_code_error": "잘못된 인증 코드", "expired_code_error": "입력한 인증 코드가 만료되었습니다", "status_sending": "전송 중...", - "status_sent": "발송 완료!", + "status_sent": "전송 완료!", "password": "비밀번호", "link_password_description": "앨범 잠금해제를 위해 비밀번호를 입력하세요", - "unlock": "", + "unlock": "잠금 해제", "set_password": "비밀번호 설정", "sign_in": "로그인", "incorrect_password": "잘못된 비밀번호입니다", - "pick_password_hint": "당신의 데이터를 암호화하는 데 사용할 수 있는 비밀번호를 입력하세요", - "pick_password_caution": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키 없다면 데이터 복구를 도와드릴 수 없습니다.", - "key_generation_in_progress": "암호 키 생성 중...", + "incorrect_password_or_no_account": "", + "pick_password_hint": "데이터 암호화를 위한 새로운 암호 입력", + "pick_password_caution": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키가 없다면 데이터 복구를 도와드릴 수 없습니다.", + "key_generation_in_progress": "암호화 키 생성 중...", "confirm_password": "비밀번호 확인", - "referral_source_hint": "어떻게 Ente에 대해 들으셨나요? (선택사항)", - "referral_source_info": "우리는 앱 설치를 추적하지 않습니다. 우리를 알게 된 곳을 남겨주시면 우리에게 도움이 될꺼에요!", + "referral_source_hint": "Ente에 대해 어떻게 알게 되었나요? (선택 사항)", + "referral_source_info": "저희는 앱 설치를 추적하지 않습니다. 어디서 저희를 찾으셨는지 알려주시면 도움이 될 거예요!", "password_mismatch_error": "비밀번호가 일치하지 않습니다", + "show_or_hide_password": "", "welcome_to_ente_title": "환영합니다 ", - "welcome_to_ente_subtitle": "End-to-End 암호화된 사진 저장 및 공유", + "welcome_to_ente_subtitle": "종간단 암호화된 사진 저장 및 공유", "new_album": "새 앨범", - "create_albums": "", - "enter_album_name": "앨범 이름", + "create_albums": "앨범 만들기", + "album_name": "앨범 이름", "close": "닫기", - "yes": "", - "no": "아니오", - "nothing_here": "", + "yes": "예", + "no": "아니요", + "nothing_here": "아직 아무것도 없습니다", "upload": "업로드", "import": "가져오기", "add_photos": "사진 추가", "add_more_photos": "사진 더 추가하기", "add_photos_count_one": "아이템 하나 추가", - "add_photos_count": "아이템 {{count, number}} 개 추가하기", - "select_photos": "사진 선택하기", + "add_photos_count": "아이템 {{count, number}}개 추가", + "select_photos": "사진 선택", "file_upload": "파일 업로드", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", + "preparing": "준비중", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "메타데이터 파일 읽는중", "upload_cancelling": "남은 업로드 취소중", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}}개 업로드 완료", + "upload_skipped": "{{count, number}}개 건너뜀", "initial_load_delay_warning": "처음 로딩시 다소 시간이 걸릴 수 있습니다", "no_account": "계정이 없습니다", "existing_account": "이미 계정이 있습니다", "create": "만들기", - "files_count": "", + "files_count": "파일 {{count, number}}개", "download": "다운로드", - "download_album": "", + "download_album": "앨범 다운로드", "download_favorites": "즐겨찾기 다운로드", - "download_uncategorized": "미분류 항목 다운로드", + "download_uncategorized": "분류되지 않은 항목 다운로드", "download_hidden_items": "숨겨진 항목 다운로드", "audio": "", "more": "", @@ -90,14 +92,14 @@ "toggle_archive": "", "view_info": "", "copy_as_png": "PNG로 복사", - "toggle_fullscreen": "전체 화면 열기", + "toggle_fullscreen": "전체 화면", "exit_fullscreen": "", "go_fullscreen": "", "zoom": "", "play": "", "pause": "", - "previous": "이전 항목", - "next": "다음 항목", + "previous": "이전", + "next": "다음", "video_seek": "", "quality": "", "auto": "", @@ -106,24 +108,24 @@ "title_photos": "Ente Photos", "title_auth": "Ente Auth", "title_accounts": "", - "upload_first_photo": "첫 번째 사진을 업로드하세요", + "upload_first_photo": "첫 사진을 업로드하세요", "import_your_folders": "폴더 가져오기", - "upload_dropzone_hint": "백업하려는 파일을 올려놓기", - "watch_folder_dropzone_hint": "추가하려는 감시 폴더 올려놓기", + "upload_dropzone_hint": "파일을 백업하려면 여기에 놓으세요", + "watch_folder_dropzone_hint": "감시할 폴더를 여기에 놓으세요", "trash_files_title": "파일들을 삭제할까요?", "trash_file_title": "파일을 삭제할까요?", - "delete_files_title": "즉시 삭제할까요?", - "delete_files_message": "선택된 파일들은 당신의 Ente계정에서 영구적으로 삭제됩니다.", + "delete_files_title": "바로 삭제할까요?", + "delete_files_message": "선택된 파일들은 당신의 Ente 계정에서 영구적으로 삭제됩니다.", "selected_count": "", "selected_and_yours_count": "", "delete": "삭제", "favorite": "즐겨찾기", "convert": "", - "multi_folder_upload": "여러 폴더들이 탐지되었다", + "multi_folder_upload": "여러 개의 폴더가 감지되었습니다", "upload_to_choice": "그것들을 업로드 하겠습니까", - "upload_to_single_album": "싱글 앨범", - "upload_to_album_per_folder": "앨범들 분리", - "session_expired": "세션 만료", + "upload_to_single_album": "하나의 앨범", + "upload_to_album_per_folder": "앨범 분리", + "session_expired": "세션 만료됨", "session_expired_message": "세션이 만료되었습니다. 계속하려면 다시 로그인하세요", "password_generation_failed": "당신의 브라우저는 Ente의 암호화 표준을 충족하는 강력한 키를 생성할 수 없습니다. 모바일 앱이나 다른 브라우저를 사용하세요.", "change_password": "암호 변경하기", @@ -132,7 +134,7 @@ "go_back": "뒤로 가기", "account": "", "recovery_key": "키 복구하기", - "do_this_later": "나중에 저장하기", + "do_this_later": "나중에", "save_key": "키 저장하기", "recovery_key_description": "암호 분실시, 오직 이 키를 이용해야만 데이터를 복구할 수 있습니다.", "key_not_stored_note": "", @@ -510,7 +512,7 @@ "today": "", "yesterday": "", "enter_name": "이름 입력", - "uploader_name_hint": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", + "uploader_name_hint": "친구들이 누가 이 멋진 사진을 찍었는지 알 수 있도록 이름을 추가하세요!", "name_placeholder": "", "more_details": "", "ml_search": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/lt-LT/translation.json b/web/packages/base/locales/lt-LT/translation.json index 8b25984fbc..419d12aa28 100644 --- a/web/packages/base/locales/lt-LT/translation.json +++ b/web/packages/base/locales/lt-LT/translation.json @@ -27,11 +27,12 @@ "status_sending": "Siunčiama...", "status_sent": "Išsiųsta.", "password": "Slaptažodis", - "link_password_description": "Įveskite slaptažodį, kad atrakintumėte albumą", + "link_password_description": "Įveskite slaptažodį, kad atrakintumėte albumą.", "unlock": "Atrakinti", "set_password": "Nustatyti slaptažodį", "sign_in": "Prisijungti", "incorrect_password": "Neteisingas slaptažodis.", + "incorrect_password_or_no_account": "Neteisingas slaptažodis arba neregistruotas el. paštas.", "pick_password_hint": "Įveskite slaptažodį, kurį galime naudoti jūsų duomenims šifruoti.", "pick_password_caution": "Jūsų slaptažodžio nesaugome, todėl jei jį pamiršite, be atkūrimo rakto negalėsime padėti atkurti duomenų.", "key_generation_in_progress": "Generuojami šifravimo raktai...", @@ -39,15 +40,16 @@ "referral_source_hint": "Kaip išgirdote apie „Ente“? (nebūtina)", "referral_source_info": "Mes nesekame programų diegimų. Mums padėtų, jei pasakytumėte, kur mus radote.", "password_mismatch_error": "Slaptažodžiai nesutampa.", + "show_or_hide_password": "Rodyti arba slėpti slaptažodį", "welcome_to_ente_title": "Sveiki atvykę į „“", "welcome_to_ente_subtitle": "Visapusiškai užšifruota nuotraukų saugykla ir bendrinimas", "new_album": "Naujas albumas", "create_albums": "Kurti albumus", - "enter_album_name": "Albumo pavadinimas", + "album_name": "Albumo pavadinimas", "close": "Užverti", "yes": "Taip", "no": "Ne", - "nothing_here": "Kol kas nieko čia nėra", + "nothing_here": "Čia dar nieko nėra.", "upload": "Įkelti", "import": "Importuoti", "add_photos": "Įtraukti nuotraukų", @@ -62,7 +64,7 @@ "upload_cancelling": "Atšaukiami likę įkėlimai", "upload_done": "{{count, number}} įkelta", "upload_skipped": "{{count, number}} praleista", - "initial_load_delay_warning": "Pirmasis įkėlimas gali šiek tiek užtrukti", + "initial_load_delay_warning": "Pirmasis įkėlimas gali šiek tiek užtrukti.", "no_account": "Neturiu paskyros", "existing_account": "Jau turiu paskyrą", "create": "Kurti", @@ -70,7 +72,7 @@ "download": "Atsisiųsti", "download_album": "Atsisiųsti albumą", "download_favorites": "Atsisiųsti mėgstamus", - "download_uncategorized": "Atsisiųsti nekategorizuotas", + "download_uncategorized": "Atsisiųsti nekategorizuotus", "download_hidden_items": "Atsisiųsti paslėptus elementus", "audio": "Garsas", "more": "Daugiau", @@ -135,7 +137,7 @@ "do_this_later": "Daryti tai vėliau", "save_key": "Išsaugoti raktą", "recovery_key_description": "Jei pamiršote slaptažodį, vienintelis būdas atkurti duomenis – naudoti šį raktą.", - "key_not_stored_note": "Šio rakto mes nesaugome, todėl išsaugokite jį saugioje vietoje", + "key_not_stored_note": "Šio rakto mes nesaugome, todėl išsaugokite jį saugioje vietoje.", "recovery_key_generation_failed": "Atkūrimo kodo nepavyko sugeneruoti. Bandykite dar kartą.", "forgot_password": "Pamiršau slaptažodį", "recover_account": "Atkurti paskyrą", @@ -150,7 +152,7 @@ "ente_help": "„Ente“ pagalba", "blog": "Tinklaraštis", "request_feature": "Prašyti funkcijos", - "support": "Pagalba", + "support": "Pagalba el. paštu", "cancel": "Atšaukti", "logout": "Atsijungti", "logout_message": "Ar tikrai norite atsijungti?", @@ -177,7 +179,7 @@ "current_usage": "Dabartinis naudojimas – {{usage}}", "two_months_free": "Gaukite 2 mėnesius nemokamai metiniuose planuose", "free_plan_option": "Tęsti su nemokamu planu", - "free_plan_description": "{{storage}} nemokamai amžinai", + "free_plan_description": "{{storage}} nemokamai amžinai.", "active": "Aktyvi", "subscription_info_free": "Esate nemokame plane", "subscription_info_family": "Esate šeimos plane, kurį valdo", @@ -247,11 +249,11 @@ "indexing_fetching": "Sinchronizuojami indeksavimai...", "indexing_people": "Sinchronizuojami asmenys...", "syncing_wait": "Sinchronizuojama...", - "people_empty_too_few": "Asmenys čia bus rodomi, kai bus pakankamai asmens nuotraukų", + "people_empty_too_few": "Asmenys čia bus rodomi, kai bus pakankamai asmens nuotraukų.", "unnamed_person": "Neįvardytas asmuo", "add_a_name": "Pridėti vardą", "new_person": "Naujas asmuo", - "add_name": "Pridėti vardą", + "add_name": "Pridėkite vardą", "rename_person": "Pervadinti asmenį", "reset_person_confirm": "Nustatyti asmenį iš naujo?", "reset_person_confirm_message": "Vardas, veidų grupavimas ir pasiūlymai šiam asmeniui bus iš naujo nustatyti", @@ -268,7 +270,7 @@ "people_suggestions_empty": "Kol kas daugiau pasiūlymų nėra", "info": "Informacija", "file_name": "Failo pavadinimas", - "caption_placeholder": "Pridėti aprašymą", + "caption_placeholder": "Pridėkite aprašą", "location": "Vietovė", "view_on_map": "Peržiūrėti žemėlapyje „OpenStreetMap“", "map": "Žemėlapis", @@ -453,7 +455,7 @@ "total_size": "Bendrą dydį", "count": "Skaičių", "deselect_all": "Naikinti visų pasirinkimą", - "no_duplicates": "Dublikatų nėra", + "no_duplicates": "Dublikatų nėra.", "duplicate_group_description": "{{count}} elementai (-ų), kiekvienas {{itemSize}}", "remove_duplicates_button_count": "Ištrinti {{count, number}} elementus (-ų)", "stop_uploads_title": "Stabdyti įkėlimus?", @@ -501,7 +503,7 @@ "uploaded_to_separate_collections": "Įkelta į atskiras kolekcijas", "nevermind": "Nesvarbu", "update_available": "Yra naujinimas", - "update_installable_message": "Nauja „Ente“ versija jau paruošta įdiegti.", + "update_installable_message": "Nauja „Ente“ versija jau parengta įdiegti.", "install_now": "Diegti dabar", "install_on_next_launch": "Diegti kito paleidimo metu", "update_available_message": "Išleista nauja „Ente“ versija, bet jos negalima automatiškai atsisiųsti ir įdiegti.", @@ -510,12 +512,12 @@ "today": "Šiandien", "yesterday": "Vakar", "enter_name": "Įveskite vardą", - "uploader_name_hint": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas.", + "uploader_name_hint": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas!", "name_placeholder": "Pavadinimas...", "more_details": "Daugiau išsamios informacijos", "ml_search": "Mašininis mokymasis", - "ml_search_description": "„Ente“ palaiko įrenginyje mašininį mokymąsi, skirtą veidų atpažinimui, magiškai paieškai ir kitoms išplėstinėms paieškos funkcijoms", - "ml_search_footnote": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „automobilis“, „raudonas automobilis“, „Ferrari“", + "ml_search_description": "„Ente“ palaiko įrenginyje mašininį mokymąsi, skirtą veidų atpažinimui, magiškai paieškai ir kitoms išplėstinėms paieškos funkcijoms.", + "ml_search_footnote": "Magiška paieška leidžia ieškoti nuotraukų pagal jų turinį, pvz., „automobilis“, „raudonas automobilis“, „Ferrari“.", "indexing": "Indeksavimas", "processed": "Apdorota", "indexing_status_running": "Vykdomas", @@ -529,9 +531,9 @@ "ml_consent_description": "

Jei įjungsite mašininį mokymąsi, „Ente“ išsitrauks tokią informaciją kaip veido geometrija iš failų, įskaitant tuos, kuriais su jumis bendrinama.

Tai bus daroma jūsų įrenginyje, o visa sugeneruota biometrinė informacija bus visapusiškai užšifruota.

Spustelėkite čia dėl išsamesnės informacijos apie šią funkciją mūsų privatumo politikoje

.", "ml_consent_confirmation": "Suprantu ir noriu įjungti mašininį mokymąsi", "labs": "Laboratorijos", - "passphrase_strength_weak": "Slaptažodžio stiprumas: silpna", - "passphrase_strength_moderate": "Slaptažodžio stiprumas: vidutinė", - "passphrase_strength_strong": "Slaptažodžio stiprumas: stipri", + "password_strength_weak": "Slaptažodžio stiprumas: silpna", + "password_strength_moderate": "Slaptažodžio stiprumas: vidutinė", + "password_strength_strong": "Slaptažodžio stiprumas: stipri", "preferences": "Nuostatos", "language": "Kalba", "advanced": "Išplėstiniai", @@ -572,9 +574,9 @@ "at": " ", "auth_next": "sekantis", "auth_download_mobile_app": "Atsisiųskite mūsų mobiliąją programą, kad tvarkytumėte savo paslaptis.", - "no_codes_added_yet": "Dar nėra pridėtų kodų", + "no_codes_added_yet": "Dar nėra pridėtų kodų.", "hide": "Slėpti", - "unhide": "Neslėpti", + "unhide": "Rodyti", "sort_by": "Rikiuoti pagal", "newest_first": "Naujausią pirmiausiai", "oldest_first": "Seniausią pirmiausiai", @@ -610,7 +612,7 @@ "rotation": "Sukimas", "rotate_left": "Sukti į kairę", "rotate_right": "Sukti į dešinę", - "flip": "Apversti", + "flip": "Apverstimas", "flip_vertically": "Apversti vertikaliai", "flip_horizontally": "Apversti horizontaliai", "download_edited": "Atsisiųsti redaguotą", @@ -622,8 +624,9 @@ "reset": "Atkurti", "faster_upload": "Spartesni įkėlimai", "faster_upload_description": "Nukreipkite įkėlimus per netoliese esančius serverius.", - "open_ente_on_startup": "Atverti „Ente“ paleidžiant", + "open_ente_on_startup": "Atverti „Ente“ paleidimo metu", "cast_album_to_tv": "Paleisti albumą televizoriuje", + "cast_to_tv": "Paleisti televizoriuje", "enter_cast_pin_code": "Įveskite žemiau esančiame televizoriuje matomą kodą, kad susietumėte šį įrenginį.", "code": "Kodas", "pair_device_to_tv": "Susieti įrenginius", @@ -661,7 +664,7 @@ "passkey_verify_description": "Patvirtinkite savo slaptaraktį, kad prisijungtumėte prie paskyros.", "waiting_for_verification": "Laukiama patvirtinimo...", "verification_still_pending": "Vis dar laukiama patvirtinimo", - "passkey_verified": "Patvirtintas slaptaraktis", + "passkey_verified": "Slaptaraktis patvirtintas", "redirecting_back_to_app": "Nukreipiama atgal į programą...", "redirect_close_instructions": "Atvėrę programą galite užverti šį langą.", "redirect_again": "Nukreipti iš naujo", @@ -674,5 +677,11 @@ "theme": "Tema", "system": "Sistemos", "light": "Šviesi", - "dark": "Tamsi" + "dark": "Tamsi", + "streamable_videos": "Srautiniai vaizdo įrašai", + "processing_videos_status": "Apdorojami vaizdo įrašai...", + "share_favorites": "Bendrinti mėgstamus", + "person_favorites": "{{name}} mėgstami", + "shared_favorites": "Bendrinami mėgstami", + "added_by_name": "Įtraukė {{name}}" } diff --git a/web/packages/base/locales/lv-LV/translation.json b/web/packages/base/locales/lv-LV/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/lv-LV/translation.json +++ b/web/packages/base/locales/lv-LV/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ml-IN/translation.json b/web/packages/base/locales/ml-IN/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/ml-IN/translation.json +++ b/web/packages/base/locales/ml-IN/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/nl-NL/translation.json b/web/packages/base/locales/nl-NL/translation.json index 80626d3762..a37071248f 100644 --- a/web/packages/base/locales/nl-NL/translation.json +++ b/web/packages/base/locales/nl-NL/translation.json @@ -32,6 +32,7 @@ "set_password": "Wachtwoord instellen", "sign_in": "Aanmelden", "incorrect_password": "Onjuist wachtwoord", + "incorrect_password_or_no_account": "Onjuist wachtwoord of e-mailadres niet geregistreerd", "pick_password_hint": "Voer een wachtwoord in dat we kunnen gebruiken om je gegevens te versleutelen", "pick_password_caution": "We slaan je wachtwoord niet op, dus als je het vergeet, zullen we u niet kunnen helpen uw data te herstellen zonder een herstelcode.", "key_generation_in_progress": "Encryptiesleutels genereren...", @@ -39,11 +40,12 @@ "referral_source_hint": "Hoe hoorde je over Ente? (optioneel)", "referral_source_info": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", "password_mismatch_error": "Wachtwoorden komen niet overeen", + "show_or_hide_password": "Toon of verberg wachtwoord", "welcome_to_ente_title": "Welkom bij ", "welcome_to_ente_subtitle": "End-to-end versleutelde foto-opslag en uitwisseling", "new_album": "Nieuw album", "create_albums": "Albums aanmaken", - "enter_album_name": "Albumnaam", + "album_name": "Albumnaam", "close": "Sluiten", "yes": "Ja", "no": "Nee", @@ -57,11 +59,11 @@ "select_photos": "Selecteer foto's", "file_upload": "Bestand uploaden", "preparing": "Voorbereiden", - "processed_counts": "", - "upload_reading_metadata_files": "", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Metadata bestanden lezen", "upload_cancelling": "Resterende uploads worden geannuleerd", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} geüpload", + "upload_skipped": "{{count, number}} overgeslagen", "initial_load_delay_warning": "Eerste keer laden kan enige tijd duren", "no_account": "Heb nog geen account", "existing_account": "Heb al een account", @@ -94,15 +96,15 @@ "exit_fullscreen": "Volledig scherm verlaten", "go_fullscreen": "Naar volledig scherm", "zoom": "Zoom", - "play": "", - "pause": "", + "play": "Afspelen", + "pause": "Pauzeren", "previous": "Vorige", "next": "Volgende", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "video_seek": "Video zoeken", + "quality": "Kwaliteit", + "auto": "Automatisch", + "original": "Origineel", + "speed": "Snelheid", "title_photos": "Ente Foto's", "title_auth": "Ente Auth", "title_accounts": "Ente Accounts", @@ -529,9 +531,9 @@ "ml_consent_description": "

Als u machine learning inschakelt, zal Ente informatie zoals gezichtsgeometrie uit bestanden extraheren, inclusief degenen die met u gedeeld worden.

Dit gebeurt op uw apparaat, en alle gegenereerde biometrische informatie zal end-to-end versleuteld worden.

Klik hier voor meer details in ons privacybeleid met betrekking tot deze functie.

", "ml_consent_confirmation": "Ik begrijp het en wil machine learning inschakelen", "labs": "Lab's", - "passphrase_strength_weak": "Wachtwoord sterkte: Zwak", - "passphrase_strength_moderate": "Wachtwoord sterkte: Matig", - "passphrase_strength_strong": "Wachtwoord sterkte: Sterk", + "password_strength_weak": "Wachtwoord sterkte: Zwak", + "password_strength_moderate": "Wachtwoord sterkte: Matig", + "password_strength_strong": "Wachtwoord sterkte: Sterk", "preferences": "Instellingen", "language": "Taal", "advanced": "Geavanceerd", @@ -624,6 +626,7 @@ "faster_upload_description": "Uploaden door nabije servers", "open_ente_on_startup": "Open Ente tijdens opstarten", "cast_album_to_tv": "Album afspelen op TV", + "cast_to_tv": "Afspelen op TV", "enter_cast_pin_code": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.", "code": "Code", "pair_device_to_tv": "Koppel apparaten", @@ -674,5 +677,11 @@ "theme": "Thema", "system": "Systeem", "light": "Licht", - "dark": "Donker" + "dark": "Donker", + "streamable_videos": "Video's met stream", + "processing_videos_status": "Video's verwerken...", + "share_favorites": "Favorieten delen", + "person_favorites": "Favorieten van {{name}}", + "shared_favorites": "Gedeelde favorieten", + "added_by_name": "Toegevoegd door {{name}}" } diff --git a/web/packages/base/locales/pl-PL/translation.json b/web/packages/base/locales/pl-PL/translation.json index 2fa4ead7b2..01371a0e60 100644 --- a/web/packages/base/locales/pl-PL/translation.json +++ b/web/packages/base/locales/pl-PL/translation.json @@ -32,6 +32,7 @@ "set_password": "Ustaw hasło", "sign_in": "Zaloguj się", "incorrect_password": "Nieprawidłowe hasło", + "incorrect_password_or_no_account": "Niepoprawne hasło lub adres e-mail nie został zarejestrowany", "pick_password_hint": "Wprowadź hasło, którego możemy użyć do zaszyfrowania Twoich danych", "pick_password_caution": "Nie przechowujemy Twojego hasła, więc jeśli je zapomnisz, nie będziemy w stanie Ci pomóc odzyskać Twoich danych bez klucza odzyskiwania.", "key_generation_in_progress": "Generowanie kluczy szyfrujących...", @@ -39,11 +40,12 @@ "referral_source_hint": "Jak usłyszałeś/aś o Ente? (opcjonalnie)", "referral_source_info": "Nie śledzimy instalacji aplikacji. Pomogłyby nam, gdybyś powiedział/a nam, gdzie nas znalazłeś/aś!", "password_mismatch_error": "Hasła nie pasują do siebie", + "show_or_hide_password": "Pokaż lub ukryj hasło", "welcome_to_ente_title": "Witamy w ", "welcome_to_ente_subtitle": "Zaszyfrowana metodą end-to-end pamięć na zdjęcia i udostępnianie", "new_album": "Nowy album", "create_albums": "Utwórz albumy", - "enter_album_name": "Nazwa albumu", + "album_name": "Nazwa albumu", "close": "Zamknij", "yes": "Tak", "no": "Nie", @@ -58,7 +60,7 @@ "file_upload": "Przesył plików", "preparing": "Przygotowywanie", "processed_counts": "", - "upload_reading_metadata_files": "", + "upload_reading_metadata_files": "Czytanie plików metadanych", "upload_cancelling": "Anulowanie pozostałych przesłań", "upload_done": "", "upload_skipped": "", @@ -73,26 +75,26 @@ "download_uncategorized": "Pobierz nieskategoryzowane", "download_hidden_items": "Pobierz ukryte elementy", "audio": "", - "more": "", + "more": "Więcej", "mouse_scroll": "Przewijanie kółkiem myszy", "pan": "", "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", + "drag": "Przeciągnij", + "tap_inside_image": "Dotknij wewnątrz obrazu", + "tap_outside_image": "Dotknij na zewnątrz obrazu", + "shortcuts": "Skróty", + "show_shortcuts": "Pokaż skróty", "zoom_preset": "", "toggle_controls": "", "toggle_live": "", "toggle_audio": "", "toggle_favorite": "", "toggle_archive": "", - "view_info": "", + "view_info": "Zobacz informacje", "copy_as_png": "Kopiuj jako PNG", "toggle_fullscreen": "Przełącz tryb pełnoekranowy", - "exit_fullscreen": "", - "go_fullscreen": "", + "exit_fullscreen": "Zamknij tryb pełnoekranowy", + "go_fullscreen": "Przejdź do trybu pełnoekranowego", "zoom": "", "play": "", "pause": "", @@ -100,7 +102,7 @@ "next": "Następny", "video_seek": "", "quality": "", - "auto": "", + "auto": "Automatycznie", "original": "", "speed": "", "title_photos": "Zdjęcia Ente", @@ -146,9 +148,9 @@ "no_recovery_key_message": "Ze względu na charakter naszego protokołu szyfrowania end-to-end, Twoje dane nie mogą być odszyfrowane bez hasła lub klucza odzyskiwania", "no_two_factor_recovery_key_message": "Wyślij wiadomość e-mail na {{emailID}} z zarejestrowanego adresu e-mail", "contact_support": "Skontaktuj się z pomocą techniczną", - "help": "", - "ente_help": "", - "blog": "", + "help": "Pomoc", + "ente_help": "Pomoc Ente", + "blog": "Blog", "request_feature": "Zaproponuj funkcję", "support": "Wsparcie Techniczne", "cancel": "Anuluj", @@ -223,8 +225,8 @@ "delete_photos": "Usuń zdjęcia", "keep_photos": "Zachowaj zdjęcia", "share_album": "Udostępnij album", - "sharing_with_self": "", - "sharing_already_shared": "", + "sharing_with_self": "Nie możesz udostępnić samemu sobie", + "sharing_already_shared": "Już udostępniasz to {{email}}", "sharing_album_not_allowed": "Udostępnianie albumu nie jest dozwolone", "sharing_disabled_for_free_accounts": "Udostępnianie jest wyłączone dla darmowych kont", "sharing_user_does_not_exist": "", @@ -492,7 +494,7 @@ "stop_watching_folder_message": "Twoje istniejące pliki nie zostaną usunięte, ale Ente przestanie automatycznie aktualizować połączony album Ente przy zmianach w tym folderze.", "yes_stop": "Tak, zatrzymaj", "change_folder": "Zmień Folder", - "view_logs": "", + "view_logs": "Wyświetl logi", "view_logs_message": "", "weak_device_hint": "Przeglądarka, której używasz nie jest wystarczająco silna, aby zaszyfrować Twoje zdjęcia. Prosimy zalogować się do Ente na Twoim komputerze lub pobierz aplikacje mobilną/komputerową Ente.", "drag_and_drop_hint": "Lub przeciągnij i upuść do okna Ente", @@ -529,9 +531,9 @@ "ml_consent_description": "

Jeśli włączysz nauczanie maszynowe, Ente wyodrębni informacje takie jak geometria twarzy z plików, w tym udostępnionych z Tobą.

Będzie to miało miejsce na Twoim urządzeniu, a wygenerowane informacje biometryczne będą zaszyfrowane end-to-end.

Kliknij tutaj, aby uzyskać więcej informacji na temat tej funkcji w naszej polityce prywatności

", "ml_consent_confirmation": "Rozumiem i chcę włączyć nauczanie maszynowe", "labs": "Laboratoria", - "passphrase_strength_weak": "Siła hasła: Słabe", - "passphrase_strength_moderate": "Siła hasła: Umiarkowane", - "passphrase_strength_strong": "Siła hasła: Silne", + "password_strength_weak": "Siła hasła: Słabe", + "password_strength_moderate": "Siła hasła: Umiarkowane", + "password_strength_strong": "Siła hasła: Silne", "preferences": "Preferencje", "language": "Język", "advanced": "Zaawansowane", @@ -593,7 +595,7 @@ "video": "Wideo", "live_photo": "Live Photo", "live": "", - "edit_image": "", + "edit_image": "Edytuj zdjęcie", "photo_editor": "Edytor zdjęć", "confirm_editor_close": "Czy na pewno chcesz zamknąć edytor?", "confirm_editor_close_message": "Pobierz edytowany obraz lub zapisz kopię do Ente, aby utrzymać zmiany.", @@ -624,6 +626,7 @@ "faster_upload_description": "Kieruj przesłania przez pobliskie serwery", "open_ente_on_startup": "", "cast_album_to_tv": "Odtwórz album na telewizorze", + "cast_to_tv": "", "enter_cast_pin_code": "Wprowadź kod, który widzisz na telewizorze poniżej, aby sparować to urządzenie.", "code": "Kod", "pair_device_to_tv": "Sparuj urządzenia", @@ -671,8 +674,14 @@ "server_endpoint": "Punkt końcowy serwera", "more_information": "Więcej informacji", "save": "Zapisz", - "theme": "", - "system": "", + "theme": "Motyw", + "system": "Systemowy", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/pt-BR/translation.json b/web/packages/base/locales/pt-BR/translation.json index c11e2b632c..27aa9f8fbf 100644 --- a/web/packages/base/locales/pt-BR/translation.json +++ b/web/packages/base/locales/pt-BR/translation.json @@ -32,6 +32,7 @@ "set_password": "Definir senha", "sign_in": "Entrar", "incorrect_password": "Senha incorreta", + "incorrect_password_or_no_account": "Senha incorreta ou e-mail não registrado", "pick_password_hint": "Insira uma senha que podemos usar para criptografar seus dados", "pick_password_caution": "Não armazenamos sua senha, portanto, caso se esqueça, não poderemos ajudar vocêa recuperar seus dados sem uma chave de recuperação.", "key_generation_in_progress": "Gerando chaves de criptografia...", @@ -39,11 +40,12 @@ "referral_source_hint": "Como você descobriu o Ente? (opcional)", "referral_source_info": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "password_mismatch_error": "As senhas não correspondem", + "show_or_hide_password": "Exibir ou ocultar senha", "welcome_to_ente_title": "Bem-vindo à ", "welcome_to_ente_subtitle": "Armazenamento de fotos e compartilhamento criptografado de ponta a ponta", "new_album": "Novo álbum", "create_albums": "Criar álbuns", - "enter_album_name": "Nome do álbum", + "album_name": "Nome do álbum", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -529,9 +531,9 @@ "ml_consent_description": "

Se você habilitar o reconhecimento facial, o aplicativo extrairá a geometria do rosto de suas fotos, inclusive aquelas compartilhadas com você.

Isso ocorrerá em seu dispositivo, e quaisquer dados biométricos gerados serão criptografados de ponta a ponta.

Por favor, clique aqui para obter mais detalhes sobre esta funcionalidade em nossa política de privacidade

", "ml_consent_confirmation": "Eu entendo, e desejo habilitar o aprendizado de máquina", "labs": "Laboratórios", - "passphrase_strength_weak": "Força da senha: fraca", - "passphrase_strength_moderate": "Força da senha: moderada", - "passphrase_strength_strong": "Força da senha: forte", + "password_strength_weak": "Força da senha: fraca", + "password_strength_moderate": "Força da senha: moderada", + "password_strength_strong": "Força da senha: forte", "preferences": "Preferências", "language": "Idioma", "advanced": "Avançado", @@ -624,6 +626,7 @@ "faster_upload_description": "Rotas enviam em servidores próximos", "open_ente_on_startup": "Abrir Ente no Início", "cast_album_to_tv": "Reproduzir álbum na TV", + "cast_to_tv": "Reproduzir na TV", "enter_cast_pin_code": "Digite o código que você vê na TV abaixo para parear este dispositivo.", "code": "Código", "pair_device_to_tv": "Parear dispositivos", @@ -674,5 +677,11 @@ "theme": "Tema", "system": "Automático", "light": "Claro", - "dark": "Escuro" + "dark": "Escuro", + "streamable_videos": "Vídeos transmissíveis", + "processing_videos_status": "Processando vídeos...", + "share_favorites": "Compartilhar favoritos", + "person_favorites": "Favoritos de {{name}}", + "shared_favorites": "Favoritos compartilhados", + "added_by_name": "Adicionado por {{name}}" } diff --git a/web/packages/base/locales/pt-PT/translation.json b/web/packages/base/locales/pt-PT/translation.json index eaee7d2610..79101c08ff 100644 --- a/web/packages/base/locales/pt-PT/translation.json +++ b/web/packages/base/locales/pt-PT/translation.json @@ -32,6 +32,7 @@ "set_password": "Definir palavra-passe", "sign_in": "Entrar", "incorrect_password": "Palavra-passe incorreta", + "incorrect_password_or_no_account": "", "pick_password_hint": "Introduza uma palavra-passe para encriptar os seus dados", "pick_password_caution": "Não guardamos a sua palavra-passe, por isso, se se esquecer dela, não poderemos ajudá-lo a recuperar os seus dados sem uma chave de recuperação.", "key_generation_in_progress": "Gerar chaves de criptografia...", @@ -39,11 +40,12 @@ "referral_source_hint": "Como é que soube do Ente? (opcional)", "referral_source_info": "Não monitorizamos as instalações de aplicações. Seria útil se nos dissesse onde nos encontrou!", "password_mismatch_error": "As palavras-passe não correspondem", + "show_or_hide_password": "", "welcome_to_ente_title": "Bem-vindo ao ", "welcome_to_ente_subtitle": "Armazenamento criptografado de ponta a ponta de fotos e compartilhamento", "new_album": "Novo álbum", "create_albums": "Criar álbuns", - "enter_album_name": "Nome do álbum", + "album_name": "Nome do álbum", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -529,9 +531,9 @@ "ml_consent_description": "

Se ativar a aprendizagem automática, o Ente extrairá informações como a geometria do rosto de ficheiros, incluindo os partilhados consigo.

Isto acontecerá no seu dispositivo e qualquer informação biométrica gerada será encriptada de ponta a ponta.

Clique aqui para obter mais detalhes sobre esta funcionalidade na nossa política de privacidade

", "ml_consent_confirmation": "Eu entendo, e desejo ativar a aprendizagem automática", "labs": "Laboratórios", - "passphrase_strength_weak": "Força da palavra-passe: Fraca", - "passphrase_strength_moderate": "Força da palavra-passe: Moderada", - "passphrase_strength_strong": "Força da palavra-passe: Forte", + "password_strength_weak": "Força da palavra-passe: Fraca", + "password_strength_moderate": "Força da palavra-passe: Moderada", + "password_strength_strong": "Força da palavra-passe: Forte", "preferences": "Preferências", "language": "Idioma", "advanced": "Avançado", @@ -624,6 +626,7 @@ "faster_upload_description": "Rotas enviam em servidores próximos", "open_ente_on_startup": "", "cast_album_to_tv": "Reproduzir álbum na TV", + "cast_to_tv": "", "enter_cast_pin_code": "Introduza o código que vê na TV abaixo para emparelhar este dispositivo.", "code": "", "pair_device_to_tv": "Emparelhar dispositivos", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ro-RO/translation.json b/web/packages/base/locales/ro-RO/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/ro-RO/translation.json +++ b/web/packages/base/locales/ro-RO/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ru-RU/translation.json b/web/packages/base/locales/ru-RU/translation.json index 6ff7af4428..044064a533 100644 --- a/web/packages/base/locales/ru-RU/translation.json +++ b/web/packages/base/locales/ru-RU/translation.json @@ -32,6 +32,7 @@ "set_password": "Установить пароль", "sign_in": "Зарегистрироваться", "incorrect_password": "Неверный пароль", + "incorrect_password_or_no_account": "", "pick_password_hint": "Пожалуйста, введите пароль, который мы можем использовать для шифрования ваших данных", "pick_password_caution": "Мы не храним ваш пароль, поэтому, если вы его забудете, мы ничем не сможем вам помочьдля восстановления ваших данных без пароля.", "key_generation_in_progress": "Генерируем ключи шифрования...", @@ -39,11 +40,12 @@ "referral_source_hint": "Как вы узнали о Ente? (необязательно)", "referral_source_info": "Будет полезно, если вы укажете, где вы узнали о нас, так как мы не отслеживаем установки приложения!", "password_mismatch_error": "Пароли не совпадают", + "show_or_hide_password": "", "welcome_to_ente_title": "Добро пожаловать в ", "welcome_to_ente_subtitle": "Сквозное зашифрованное хранение фотографий и общий доступ к ним", "new_album": "Новый альбом", "create_albums": "Создать альбомы", - "enter_album_name": "Название альбома", + "album_name": "Название альбома", "close": "Закрыть", "yes": "Да", "no": "Нет", @@ -529,9 +531,9 @@ "ml_consent_description": "

Если вы включите машинное обучение, Ente будет извлекать информацию из файлов (например, геометрию лица), включая те, которыми с вами поделились.

Это будет происходить на вашем устройстве, и любая сгенерированная биометрическая информация будет зашифрована с использованием сквозного (End-to-End) шифрования

Пожалуйста нажмите здесь для получения дополнительной информации об этой функции в нашей политике конфиденциальности

", "ml_consent_confirmation": "Я понимаю, и хочу разрешить машинное обучение", "labs": "Лаборатории", - "passphrase_strength_weak": "Надежность пароля: слабая", - "passphrase_strength_moderate": "Надежность пароля: Умеренная", - "passphrase_strength_strong": "Надежность пароля: Надежный", + "password_strength_weak": "Надежность пароля: слабая", + "password_strength_moderate": "Надежность пароля: Умеренная", + "password_strength_strong": "Надежность пароля: Надежный", "preferences": "Настройки", "language": "Язык", "advanced": "Расширенные", @@ -624,6 +626,7 @@ "faster_upload_description": "Загрузка маршрута через близлежащие серверы", "open_ente_on_startup": "", "cast_album_to_tv": "Воспроизвести альбом на ТВ", + "cast_to_tv": "", "enter_cast_pin_code": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.", "code": "Код", "pair_device_to_tv": "Сопряжение устройств", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/sl-SI/translation.json b/web/packages/base/locales/sl-SI/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/sl-SI/translation.json +++ b/web/packages/base/locales/sl-SI/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/sr-SP/translation.json b/web/packages/base/locales/sr-SP/translation.json new file mode 100644 index 0000000000..163fa57fa5 --- /dev/null +++ b/web/packages/base/locales/sr-SP/translation.json @@ -0,0 +1,687 @@ +{ + "intro_slide_1_title": "", + "intro_slide_1": "", + "intro_slide_2_title": "", + "intro_slide_2": "", + "intro_slide_3_title": "", + "intro_slide_3": "", + "login": "", + "sign_up": "", + "new_to_ente": "", + "existing_user": "", + "enter_email": "", + "invalid_email_error": "", + "required": "", + "email_not_registered": "", + "email_already_registered": "", + "email_sent": "", + "check_inbox_hint": "", + "verification_code": "", + "resend_code": "", + "verify": "", + "send_otp": "", + "generic_error": "", + "generic_error_retry": "", + "invalid_code_error": "", + "expired_code_error": "", + "status_sending": "", + "status_sent": "", + "password": "", + "link_password_description": "", + "unlock": "", + "set_password": "", + "sign_in": "", + "incorrect_password": "", + "incorrect_password_or_no_account": "", + "pick_password_hint": "", + "pick_password_caution": "", + "key_generation_in_progress": "", + "confirm_password": "", + "referral_source_hint": "", + "referral_source_info": "", + "password_mismatch_error": "", + "show_or_hide_password": "", + "welcome_to_ente_title": "", + "welcome_to_ente_subtitle": "", + "new_album": "", + "create_albums": "", + "album_name": "", + "close": "", + "yes": "", + "no": "", + "nothing_here": "", + "upload": "", + "import": "", + "add_photos": "", + "add_more_photos": "", + "add_photos_count_one": "", + "add_photos_count": "", + "select_photos": "", + "file_upload": "", + "preparing": "", + "processed_counts": "", + "upload_reading_metadata_files": "", + "upload_cancelling": "", + "upload_done": "", + "upload_skipped": "", + "initial_load_delay_warning": "", + "no_account": "", + "existing_account": "", + "create": "", + "files_count": "", + "download": "", + "download_album": "", + "download_favorites": "", + "download_uncategorized": "", + "download_hidden_items": "", + "audio": "", + "more": "", + "mouse_scroll": "", + "pan": "", + "pinch": "", + "drag": "", + "tap_inside_image": "", + "tap_outside_image": "", + "shortcuts": "", + "show_shortcuts": "", + "zoom_preset": "", + "toggle_controls": "", + "toggle_live": "", + "toggle_audio": "", + "toggle_favorite": "", + "toggle_archive": "", + "view_info": "", + "copy_as_png": "", + "toggle_fullscreen": "", + "exit_fullscreen": "", + "go_fullscreen": "", + "zoom": "", + "play": "", + "pause": "", + "previous": "", + "next": "", + "video_seek": "", + "quality": "", + "auto": "", + "original": "", + "speed": "", + "title_photos": "", + "title_auth": "", + "title_accounts": "", + "upload_first_photo": "", + "import_your_folders": "", + "upload_dropzone_hint": "", + "watch_folder_dropzone_hint": "", + "trash_files_title": "", + "trash_file_title": "", + "delete_files_title": "", + "delete_files_message": "", + "selected_count": "", + "selected_and_yours_count": "", + "delete": "", + "favorite": "", + "convert": "", + "multi_folder_upload": "", + "upload_to_choice": "", + "upload_to_single_album": "", + "upload_to_album_per_folder": "", + "session_expired": "", + "session_expired_message": "", + "password_generation_failed": "", + "change_password": "", + "password_changed_elsewhere": "", + "password_changed_elsewhere_message": "", + "go_back": "", + "account": "", + "recovery_key": "", + "do_this_later": "", + "save_key": "", + "recovery_key_description": "", + "key_not_stored_note": "", + "recovery_key_generation_failed": "", + "forgot_password": "", + "recover_account": "", + "recover": "", + "no_recovery_key_title": "", + "incorrect_recovery_key": "", + "sorry": "", + "no_recovery_key_message": "", + "no_two_factor_recovery_key_message": "", + "contact_support": "", + "help": "", + "ente_help": "", + "blog": "", + "request_feature": "", + "support": "", + "cancel": "", + "logout": "", + "logout_message": "", + "delete_account": "", + "delete_account_manually_message": "", + "change_email": "", + "ok": "", + "success": "", + "error": "", + "offline_message": "", + "install": "", + "install_mobile_app": "", + "download_app": "", + "download_app_message": "", + "subscription": "", + "manage_payment_method": "", + "manage_family": "", + "family_plan": "", + "leave_family_plan": "", + "leave": "", + "leave_family_plan_confirm": "", + "choose_plan": "", + "manage_plan": "", + "current_usage": "", + "two_months_free": "", + "free_plan_option": "", + "free_plan_description": "", + "active": "", + "subscription_info_free": "", + "subscription_info_family": "", + "subscription_info_expired": "", + "subscription_info_renewal_cancelled": "", + "subscription_info_storage_quota_exceeded": "", + "subscription_status_renewal_active": "", + "subscription_status_renewal_cancelled": "", + "add_on_valid_till": "", + "subscription_expired": "", + "storage_quota_exceeded": "", + "subscription_purchase_success": "", + "subscription_purchase_cancelled": "", + "subscription_purchase_failed": "", + "subscription_verification_error": "", + "update_payment_method_message": "", + "payment_method_authentication_failed": "", + "update_payment_method": "", + "monthly": "", + "yearly": "", + "month_short": "", + "year": "", + "update_subscription": "", + "update_subscription_title": "", + "update_subscription_message": "", + "cancel_subscription": "", + "cancel_subscription_message": "", + "cancel_subscription_with_addon_message": "", + "subscription_cancel_success": "", + "reactivate_subscription": "", + "reactivate_subscription_message": "", + "subscription_activate_success": "", + "thank_you": "", + "cancel_subscription_on_mobile": "", + "cancel_subscription_on_mobile_message": "", + "mail_to_manage_subscription": "", + "rename": "", + "rename_file": "", + "rename_album": "", + "delete_album": "", + "delete_album_title": "", + "delete_album_message": "", + "delete_photos": "", + "keep_photos": "", + "share_album": "", + "sharing_with_self": "", + "sharing_already_shared": "", + "sharing_album_not_allowed": "", + "sharing_disabled_for_free_accounts": "", + "sharing_user_does_not_exist": "", + "search": "", + "search_results": "", + "no_results": "", + "search_hint": "", + "album": "", + "date": "", + "description": "", + "file_type": "", + "magic": "", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count": "", + "terms_and_conditions": "", + "people": "", + "indexing_scheduled": "", + "indexing_photos": "", + "indexing_fetching": "", + "indexing_people": "", + "syncing_wait": "", + "people_empty_too_few": "", + "unnamed_person": "", + "add_a_name": "", + "new_person": "", + "add_name": "", + "rename_person": "", + "reset_person_confirm": "", + "reset_person_confirm_message": "", + "ignore": "", + "ignore_person_confirm": "", + "ignore_person_confirm_message": "", + "ignored": "", + "show_person": "", + "review_suggestions": "", + "saved_choices": "", + "discard_changes": "", + "discard_changes_confirm_message": "", + "people_suggestions_finding": "", + "people_suggestions_empty": "", + "info": "", + "file_name": "", + "caption_placeholder": "", + "location": "", + "view_on_map": "", + "map": "", + "enable_map": "", + "enable_maps_confirm": "", + "enable_maps_confirm_message": "", + "disable_map": "", + "disable_maps_confirm": "", + "disable_maps_confirm_message": "", + "details": "", + "view_exif": "", + "no_exif": "", + "exif": "", + "two_factor": "", + "two_factor_authentication": "", + "two_factor_qr_help": "", + "two_factor_manual_entry_title": "", + "two_factor_manual_entry_message": "", + "scan_qr_title": "", + "enable_two_factor": "", + "enable": "", + "enabled": "", + "lost_2fa_device": "", + "incorrect_code": "", + "two_factor_info": "", + "disable": "", + "reconfigure": "", + "reconfigure_two_factor_hint": "", + "update_two_factor": "", + "update_two_factor_message": "", + "update": "", + "disable_two_factor": "", + "disable_two_factor_message": "", + "export_data": "", + "select_folder": "", + "select_zips": "", + "faq": "", + "takeout_hint": "", + "destination": "", + "start": "", + "last_export_time": "", + "export_again": "", + "local_storage_not_accessible": "", + "email_already_taken": "", + "live_photos_detected": "", + "ignored_uploads": "", + "ignored_uploads_hint": "", + "file_not_uploaded_list": "", + "failed_uploads": "", + "failed_uploads_hint": "", + "retry_failed_uploads": "", + "thumbnail_generation_failed": "", + "thumbnail_generation_failed_hint": "", + "unsupported_files": "", + "unsupported_files_hint": "", + "blocked_uploads": "", + "blocked_uploads_hint": "", + "large_files": "", + "large_files_hint": "", + "insufficient_storage": "", + "insufficient_storage_hint": "", + "uploads_in_progress": "", + "successful_uploads": "", + "upload_to_album": "", + "add_to_album": "", + "move_to_album": "", + "unhide_to_album": "", + "restore_to_album": "", + "section_all": "", + "section_uncategorized": "", + "section_archive": "", + "section_hidden": "", + "section_trash": "", + "favorites": "", + "archive": "", + "archive_album": "", + "unarchive": "", + "unarchive_album": "", + "hide_collection": "", + "unhide_collection": "", + "move": "", + "add": "", + "remove": "", + "yes_remove": "", + "remove_from_album": "", + "move_to_trash": "", + "trash_files_message": "", + "trash_file_message": "", + "delete_permanently": "", + "restore": "", + "empty_trash": "", + "empty_trash_title": "", + "empty_trash_message": "", + "leave_album": "", + "leave_shared_album_title": "", + "leave_shared_album_message": "", + "leave_shared_album": "", + "confirm_remove_message": "", + "confirm_remove_incl_others_message": "", + "oldest": "", + "last_updated": "", + "name": "", + "fix_creation_time": "", + "fix_creation_time_in_progress": "", + "fix_creation_time_file_updated": "", + "fix_creation_time_completed": "", + "fix_creation_time_completed_with_errors": "", + "fix_creation_time_options": "", + "exif_date_time_original": "", + "exif_date_time_digitized": "", + "exif_metadata_date": "", + "custom_time": "", + "caption_character_limit": "", + "sharing_details": "", + "modify_sharing": "", + "add_collaborators": "", + "add_new_email": "", + "shared_with_people_count_zero": "", + "shared_with_people_count_one": "", + "shared_with_people_count": "", + "participants_count_zero": "", + "participants_count_one": "", + "participants_count": "", + "add_viewers": "", + "change_permission_to_viewer": "", + "change_permission_to_collaborator": "", + "change_permission_title": "", + "confirm_convert_to_viewer": "", + "confirm_convert_to_collaborator": "", + "manage": "", + "added_as": "", + "collaborator_hint": "", + "remove_participant": "", + "remove_participant_title": "", + "remove_participant_message": "", + "confirm_remove": "", + "owner": "", + "collaborators": "", + "viewers": "", + "add_more": "", + "or_add_existing": "", + "not_found": "", + "link_expired": "", + "link_expired_message": "", + "manage_link": "", + "link_request_limit_exceeded": "", + "allow_downloads": "", + "allow_adding_photos": "", + "allow_adding_photos_hint": "", + "device_limit": "", + "none": "", + "link_expiry": "", + "never": "", + "after_time": { + "hour": "", + "day": "", + "week": "", + "month": "", + "year": "" + }, + "copy_link": "", + "done": "", + "share_link_section_title": "", + "remove_link": "", + "create_public_link": "", + "public_link_created": "", + "public_link_enabled": "", + "collect_photos": "", + "disable_file_download": "", + "disable_file_download_message": "", + "shared_using": "", + "sharing_referral_code": "", + "disable_password": "", + "disable_password_message": "", + "password_lock": "", + "lock": "", + "file": "", + "folder": "", + "google_takeout": "", + "deduplicate_files": "", + "remove_duplicates": "", + "total_size": "", + "count": "", + "deselect_all": "", + "no_duplicates": "", + "duplicate_group_description": "", + "remove_duplicates_button_count": "", + "stop_uploads_title": "", + "stop_uploads_message": "", + "yes_stop_uploads": "", + "stop_downloads_title": "", + "stop_downloads_message": "", + "yes_stop_downloads": "", + "albums": "", + "albums_count_one": "", + "albums_count": "", + "all_albums": "", + "all_hidden_albums": "", + "hidden_albums": "", + "hidden_items": "", + "enter_two_factor_otp": "", + "create_account": "", + "copied": "", + "upgrade_now": "", + "renew_now": "", + "storage": "", + "used": "", + "you": "", + "family": "", + "free": "", + "of": "", + "watch_folders": "", + "watched_folders": "", + "no_folders_added": "", + "watch_folders_hint_1": "", + "watch_folders_hint_2": "", + "watch_folders_hint_3": "", + "add_folder": "", + "stop_watching": "", + "stop_watching_folder_title": "", + "stop_watching_folder_message": "", + "yes_stop": "", + "change_folder": "", + "view_logs": "", + "view_logs_message": "", + "weak_device_hint": "", + "drag_and_drop_hint": "", + "authenticate": "", + "uploaded_to_single_collection": "", + "uploaded_to_separate_collections": "", + "nevermind": "", + "update_available": "", + "update_installable_message": "", + "install_now": "", + "install_on_next_launch": "", + "update_available_message": "", + "download_and_install": "", + "ignore_this_version": "", + "today": "", + "yesterday": "", + "enter_name": "", + "uploader_name_hint": "", + "name_placeholder": "", + "more_details": "", + "ml_search": "", + "ml_search_description": "", + "ml_search_footnote": "", + "indexing": "", + "processed": "", + "indexing_status_running": "", + "indexing_status_fetching": "", + "indexing_status_scheduled": "", + "indexing_status_done": "", + "ml_search_disable": "", + "ml_search_disable_confirm": "", + "ml_consent": "", + "ml_consent_title": "", + "ml_consent_description": "", + "ml_consent_confirmation": "", + "labs": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", + "preferences": "", + "language": "", + "advanced": "", + "export_directory_does_not_exist": "", + "export_directory_does_not_exist_message": "", + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" + }, + "stop": "", + "sync_continuously": "", + "export_starting": "", + "export_preparing": "", + "export_renaming_album_folders": "", + "export_trashing_deleted_files": "", + "export_trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", + "delete_account_reason_label": "", + "delete_account_reason_placeholder": "", + "delete_reason": { + "missing_feature": "", + "behaviour": "", + "found_another_service": "", + "not_listed": "" + }, + "delete_account_feedback_label": "", + "delete_account_feedback_placeholder": "", + "delete_account_confirm_checkbox_label": "", + "delete_account_confirm": "", + "delete_account_confirm_message": "", + "feedback_required": "", + "feedback_required_found_another_service": "", + "recover_two_factor": "", + "at": "", + "auth_next": "", + "auth_download_mobile_app": "", + "no_codes_added_yet": "", + "hide": "", + "unhide": "", + "sort_by": "", + "newest_first": "", + "oldest_first": "", + "pin_album": "", + "unpin_album": "", + "unpreviewable_file_message": "", + "download_complete": "", + "downloading_album": "", + "download_failed": "", + "download_progress": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", + "image": "", + "video": "", + "live_photo": "", + "live": "", + "edit_image": "", + "photo_editor": "", + "confirm_editor_close": "", + "confirm_editor_close_message": "", + "brightness": "", + "contrast": "", + "saturation": "", + "blur": "", + "transform": "", + "crop": "", + "aspect_ratio": "", + "square": "", + "freehand": "", + "apply_crop": "", + "rotation": "", + "rotate_left": "", + "rotate_right": "", + "flip": "", + "flip_vertically": "", + "flip_horizontally": "", + "download_edited": "", + "save_a_copy_to_ente": "", + "restore_original": "", + "photo_edit_required_to_save": "", + "colors": "", + "invert_colors": "", + "reset": "", + "faster_upload": "", + "faster_upload_description": "", + "open_ente_on_startup": "", + "cast_album_to_tv": "", + "cast_to_tv": "", + "enter_cast_pin_code": "", + "code": "", + "pair_device_to_tv": "", + "tv_not_found": "", + "cast_auto_pair": "", + "cast_auto_pair_description": "", + "choose_device_from_browser": "", + "cast_auto_pair_failed": "", + "pair_with_pin": "", + "pair_with_pin_description": "", + "visit_cast_url": "", + "passkeys": "", + "passkey_fetch_failed": "", + "manage_passkey": "", + "delete_passkey": "", + "delete_passkey_confirmation": "", + "rename_passkey": "", + "add_passkey": "", + "enter_passkey_name": "", + "passkeys_description": "", + "created_at": "", + "passkey_add_failed": "", + "passkey_login_failed": "", + "passkey_login_invalid_url": "", + "passkey_login_already_claimed_session": "", + "passkey_login_generic_error": "", + "passkey_login_credential_hint": "", + "passkeys_not_supported": "", + "try_again": "", + "check_status": "", + "passkey_login_instructions": "", + "passkey_login": "", + "totp_login": "", + "passkey": "", + "passkey_verify_description": "", + "waiting_for_verification": "", + "verification_still_pending": "", + "passkey_verified": "", + "redirecting_back_to_app": "", + "redirect_close_instructions": "", + "redirect_again": "", + "autogenerated_first_album_name": "", + "autogenerated_default_album_name": "", + "developer_settings": "", + "server_endpoint": "", + "more_information": "", + "save": "", + "theme": "", + "system": "", + "light": "", + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" +} diff --git a/web/packages/base/locales/sv-SE/translation.json b/web/packages/base/locales/sv-SE/translation.json index 693a176508..3844f94a77 100644 --- a/web/packages/base/locales/sv-SE/translation.json +++ b/web/packages/base/locales/sv-SE/translation.json @@ -32,18 +32,20 @@ "set_password": "Ställ in lösenord", "sign_in": "Logga in", "incorrect_password": "Felaktigt lösenord", + "incorrect_password_or_no_account": "", "pick_password_hint": "Ange ett lösenord som vi kan använda för att kryptera din data", "pick_password_caution": "Vi lagrar inte ditt lösenord, så om du glömmer det, vi kommer inte att kunna hjälpa dig återställa dina data utan en återställningsnyckel.", "key_generation_in_progress": "Genererar krypteringsnycklar...", "confirm_password": "Bekräfta lösenord", "referral_source_hint": "Hur hörde du talas om Ente? (valfritt)", - "referral_source_info": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!", + "referral_source_info": "Vi spårar inte appinstallationer, det skulle hjälpa oss om du berättade var du hittade oss!", "password_mismatch_error": "Lösenorden matchar inte", + "show_or_hide_password": "", "welcome_to_ente_title": "Välkommen till ", "welcome_to_ente_subtitle": "Totalsträckskrypterad lagring och delning av foton", "new_album": "Nytt album", "create_albums": "Skapa album", - "enter_album_name": "Albumnamn", + "album_name": "Albumnamn", "close": "Stäng", "yes": "Ja", "no": "Nej", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "Jag förstår och vill aktivera maskininlärning", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "Språk", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "Spela album på TV", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ta-IN/translation.json b/web/packages/base/locales/ta-IN/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/ta-IN/translation.json +++ b/web/packages/base/locales/ta-IN/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/te-IN/translation.json b/web/packages/base/locales/te-IN/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/te-IN/translation.json +++ b/web/packages/base/locales/te-IN/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/th-TH/translation.json b/web/packages/base/locales/th-TH/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/th-TH/translation.json +++ b/web/packages/base/locales/th-TH/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/ti-ER/translation.json b/web/packages/base/locales/ti-ER/translation.json index 710444281b..163fa57fa5 100644 --- a/web/packages/base/locales/ti-ER/translation.json +++ b/web/packages/base/locales/ti-ER/translation.json @@ -32,6 +32,7 @@ "set_password": "", "sign_in": "", "incorrect_password": "", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "", "welcome_to_ente_title": "", "welcome_to_ente_subtitle": "", "new_album": "", "create_albums": "", - "enter_album_name": "", + "album_name": "", "close": "", "yes": "", "no": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", + "password_strength_weak": "", + "password_strength_moderate": "", + "password_strength_strong": "", "preferences": "", "language": "", "advanced": "", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/tr-TR/translation.json b/web/packages/base/locales/tr-TR/translation.json index 17c627f887..0060830422 100644 --- a/web/packages/base/locales/tr-TR/translation.json +++ b/web/packages/base/locales/tr-TR/translation.json @@ -1,10 +1,10 @@ { - "intro_slide_1_title": "", - "intro_slide_1": "", - "intro_slide_2_title": "", - "intro_slide_2": "", - "intro_slide_3_title": "", - "intro_slide_3": "", + "intro_slide_1_title": "Anılarınız için
özel yedeklemeler", + "intro_slide_1": "Varsayılan olarak uçtan uca şifrelenmiştir", + "intro_slide_2_title": "Güvenle saklanır
bir nükleer sığınakta", + "intro_slide_2": "Her şeye rağmen ayakta kalacak şekilde tasarlandı", + "intro_slide_3_title": "Her yerden
erişilebilir", + "intro_slide_3": "Android, iOS, Web, Masaüstü", "login": "Giriş yap", "sign_up": "Hesap aç", "new_to_ente": "Yeni ente kullanıcısı", @@ -12,15 +12,15 @@ "enter_email": "E-posta adresini girin", "invalid_email_error": "Geçerli bir e-posta gir", "required": "Gerekli", - "email_not_registered": "", - "email_already_registered": "", + "email_not_registered": "E-posta kayıtlı değil", + "email_already_registered": "E-posta zaten kayıtlı", "email_sent": "Doğrulama kodu
{{email}} adresine gönderildi", "check_inbox_hint": "Lütfen doğrulama işlemini tamamlamak için gelen kutusunu (veya spam) kontrol et", "verification_code": "Doğrulama kodu", "resend_code": "Kodu yeniden gönder", "verify": "Doğrula", - "send_otp": "", - "generic_error": "", + "send_otp": "OTP'yi gönder", + "generic_error": "Bir şeyler ters gitti", "generic_error_retry": "Bir şeyler ters gitti, lütfen tekrar dene", "invalid_code_error": "Geçersiz doğrulama kodu", "expired_code_error": "Doğrulama kodunun süresi doldu", @@ -32,109 +32,111 @@ "set_password": "Parola ayarla", "sign_in": "Giriş yap", "incorrect_password": "Yanlış parola", + "incorrect_password_or_no_account": "Yanlış şifre veya e-posta kayıtlı değil", "pick_password_hint": "Lütfen verilerini şifrelemek için kullanabileceğimiz bir parola gir", "pick_password_caution": "Parolanı saklamıyoruz, dolayısıyla unutursan kurtarma anahtarın olmadan verilerini kurtarmana yardım edemeyiz.", "key_generation_in_progress": "Şifreleme anahtarları oluşturuluyor...", "confirm_password": "Parolayı onayla", - "referral_source_hint": "Ente'yi nereden duydun? (opsiyonel)", + "referral_source_hint": "Ente'yi nereden duydun? (tercihe bağlı)", "referral_source_info": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğundan bahsetmen bize çok yardımcı olacak!", "password_mismatch_error": "Parolalar eşleşmiyor", + "show_or_hide_password": "Şifreyi göster veya gizle", "welcome_to_ente_title": "'ye hoş geldin", "welcome_to_ente_subtitle": "Uçtan uca şifreli fotoğraf depolama ve paylaşımı", "new_album": "Yeni albüm", "create_albums": "Albüm oluştur", - "enter_album_name": "Albüm adı", + "album_name": "Albüm adı", "close": "Kapat", - "yes": "", + "yes": "Evet", "no": "Hayır", - "nothing_here": "", - "upload": "", - "import": "", - "add_photos": "", - "add_more_photos": "", - "add_photos_count_one": "", - "add_photos_count": "", - "select_photos": "", - "file_upload": "", - "preparing": "", - "processed_counts": "", - "upload_reading_metadata_files": "", - "upload_cancelling": "", - "upload_done": "", - "upload_skipped": "", - "initial_load_delay_warning": "", - "no_account": "", - "existing_account": "", - "create": "", - "files_count": "", - "download": "", - "download_album": "", - "download_favorites": "", - "download_uncategorized": "", - "download_hidden_items": "", - "audio": "", - "more": "", - "mouse_scroll": "", - "pan": "", - "pinch": "", - "drag": "", - "tap_inside_image": "", - "tap_outside_image": "", - "shortcuts": "", - "show_shortcuts": "", - "zoom_preset": "", - "toggle_controls": "", - "toggle_live": "", - "toggle_audio": "", - "toggle_favorite": "", - "toggle_archive": "", - "view_info": "", - "copy_as_png": "", - "toggle_fullscreen": "", - "exit_fullscreen": "", - "go_fullscreen": "", - "zoom": "", - "play": "", - "pause": "", - "previous": "", - "next": "", - "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", - "title_photos": "", - "title_auth": "", - "title_accounts": "", - "upload_first_photo": "", - "import_your_folders": "", - "upload_dropzone_hint": "", - "watch_folder_dropzone_hint": "", - "trash_files_title": "", - "trash_file_title": "", - "delete_files_title": "", - "delete_files_message": "", - "selected_count": "", - "selected_and_yours_count": "", - "delete": "", - "favorite": "", - "convert": "", + "nothing_here": "Henüz burada hiçbir şey yok", + "upload": "Yükle", + "import": "İçeri aktar", + "add_photos": "Fotoğraf ekle", + "add_more_photos": "Daha fazla fotoğraf ekle", + "add_photos_count_one": "1 öğe ekle", + "add_photos_count": "{{count, number}} Öğe ekle", + "select_photos": "Fotoğraf seç", + "file_upload": "Dosya yükle", + "preparing": "Hazırlanıyor", + "processed_counts": "{{count, number}} / {{total, number}}", + "upload_reading_metadata_files": "Meta veri dosyaları okunuyor", + "upload_cancelling": "Kalan yüklemeler iptal ediliyor", + "upload_done": "{{count, number}} dosya yüklendi", + "upload_skipped": "{{count, number}} dosya atlandı", + "initial_load_delay_warning": "İlk yükleme biraz zaman alabilir", + "no_account": "Hesabınız yok", + "existing_account": "Zaten bir hesabınız var", + "create": "Oluştur", + "files_count": "{{count, number}} dosya", + "download": "İndir", + "download_album": "Albümü indir", + "download_favorites": "Favorileri indir", + "download_uncategorized": "Kategorize edilmemiş dosyaları indir", + "download_hidden_items": "Gizli öğeleri indir", + "audio": "Ses", + "more": "Daha fazla", + "mouse_scroll": "Fare tekerleği kaydırma", + "pan": "Kaydır", + "pinch": "Sıkıştır", + "drag": "Sürükle", + "tap_inside_image": "Resmin içine dokunun", + "tap_outside_image": "Resmin dışına dokunun", + "shortcuts": "Kısayollar", + "show_shortcuts": "Kısayolları göster", + "zoom_preset": "Yakınlaştırma ayarı", + "toggle_controls": "Kontrolleri değiştir", + "toggle_live": "Canlıyı aç/kapat", + "toggle_audio": "Sesi aç/kapat", + "toggle_favorite": "Favorilere ekle/kaldır", + "toggle_archive": "Arşive ekle/kaldır", + "view_info": "Bilgileri görüntüle", + "copy_as_png": "PNG olarak kopyala", + "toggle_fullscreen": "Tam ekrana geç/çık", + "exit_fullscreen": "Tam ekrandan çık", + "go_fullscreen": "Tam ekrana geç", + "zoom": "Yakınlaştır", + "play": "Oynat", + "pause": "Duraklat", + "previous": "Önceki", + "next": "Sonraki", + "video_seek": "Videoda atlama", + "quality": "Kalite", + "auto": "Otomatik", + "original": "Orijinal", + "speed": "Hız", + "title_photos": "Ente Fotoğraflar", + "title_auth": "Ente Kimlik Doğrulaması", + "title_accounts": "Ente Hesapları", + "upload_first_photo": "İlk fotoğrafını yükle", + "import_your_folders": "Klasörlerinizi içe aktarın", + "upload_dropzone_hint": "Dosyalarınızı yedeklemek için sürükleyip bırakın", + "watch_folder_dropzone_hint": "İzlenen klasör eklemek için sürükleyip bırakın", + "trash_files_title": "Dosyalar silinsin mi?", + "trash_file_title": "Dosya silinsin mi?", + "delete_files_title": "Hemen silinsin mi?", + "delete_files_message": "Seçilen dosyalar Ente hesabınızdan kalıcı olarak silinecek.", + "selected_count": "{{selected, number}} dosya seçildi", + "selected_and_yours_count": "{{selected, number}} dosya seçildi, {{yours, number}} tanesi size ait", + "delete": "Sil", + "favorite": "Favori", + "convert": "Dönüştür", "multi_folder_upload": "Birden fazla klasör algılandı", - "upload_to_choice": "Bunları yüklemek ister misin?", + "upload_to_choice": "Bunları şu klasöre yüklemek ister misiniz", "upload_to_single_album": "Tek bir albüm", "upload_to_album_per_folder": "Ayrı ayrı albümler", "session_expired": "Oturum süresi doldu", "session_expired_message": "Oturum sona erdi, devam etmek için lütfen tekrar giriş yap", - "password_generation_failed": "Tarayıcın Ente'nin şifreleme standartlarını karşılayan güçlü bir anahtar üretemedi, lütfen mobil uygulamayı veya başka bir tarayıcıyı kullanmayı dene", + "password_generation_failed": "Tarayıcın Ente'nin şifreleme standartlarını karşılayan güçlü bir anahtar üretemedi, lütfen mobil uygulamayı veya başka bir tarayıcı kullanmayı dene", "change_password": "Parolayı değiştir", "password_changed_elsewhere": "Parola başka bir yerde değiştirildi", - "password_changed_elsewhere_message": "Lütfen yeni parolanı kullanarak kimlik doğrulaması yapmak için bu cihazda tekrar oturum aç.", + "password_changed_elsewhere_message": "Yeni şifrenizle kimlik doğrulamak için lütfen bu cihazda tekrar oturum açın.", "go_back": "Geri dön", - "account": "", + "account": "Hesap", "recovery_key": "Kurtarma anahtarı", "do_this_later": "Sonra yap", "save_key": "Anahtarı kaydet", - "recovery_key_description": "Eğer parolanı unutursan, verilerini kurtarabileceğin tek yol bu anahtardır.", + "recovery_key_description": "Eğer parolanı unutursan, verilerini sadece bu anahtarla kurtarabilirsin.", "key_not_stored_note": "Bu anahtarı saklamıyoruz, bu nedenle lütfen güvenli bir yerde sakla", "recovery_key_generation_failed": "Kurtarma kodu oluşturulamadı, lütfen tekrar dene", "forgot_password": "Parolamı unuttum", @@ -146,9 +148,9 @@ "no_recovery_key_message": "Uçtan uca şifreleme protokolümüzün doğası gereği, verilerin parolan veya kurtarma anahtarın olmadan çözülemez", "no_two_factor_recovery_key_message": "Lütfen kaydolduğun e-posta adresinden {{emailID}} adresine bir e-posta bırak", "contact_support": "Destek ile iletişime geç", - "help": "", - "ente_help": "", - "blog": "", + "help": "Yardım", + "ente_help": "Ente Yardım", + "blog": "Blog", "request_feature": "Özellik İste", "support": "Destek", "cancel": "İptal", @@ -161,518 +163,525 @@ "success": "Başarılı", "error": "Hata", "offline_message": "Çevrimdışısın, önbelleğe alınmış anılar gösteriliyor", - "install": "", + "install": "Kur", "install_mobile_app": "Tüm fotoğraflarını otomatik olarak yedeklemek için Android veya iOS uygulamamızı yükle", "download_app": "Masaüstü uygulamasını indir", "download_app_message": "Üzgünüz, bu işlem şu anda yalnızca masaüstü uygulamamızda destekleniyor", "subscription": "Abonelik", "manage_payment_method": "Ödeme yöntemini ayarla", - "manage_family": "", - "family_plan": "", - "leave_family_plan": "", - "leave": "", - "leave_family_plan_confirm": "", - "choose_plan": "", - "manage_plan": "", - "current_usage": "", - "two_months_free": "", - "free_plan_option": "", - "free_plan_description": "", - "active": "", - "subscription_info_free": "", - "subscription_info_family": "", - "subscription_info_expired": "", - "subscription_info_renewal_cancelled": "", - "subscription_info_storage_quota_exceeded": "", - "subscription_status_renewal_active": "", - "subscription_status_renewal_cancelled": "", - "add_on_valid_till": "", - "subscription_expired": "", - "storage_quota_exceeded": "", - "subscription_purchase_success": "", - "subscription_purchase_cancelled": "", - "subscription_purchase_failed": "", - "subscription_verification_error": "", - "update_payment_method_message": "", - "payment_method_authentication_failed": "", - "update_payment_method": "", - "monthly": "", - "yearly": "", - "month_short": "", - "year": "", - "update_subscription": "", - "update_subscription_title": "", - "update_subscription_message": "", - "cancel_subscription": "", - "cancel_subscription_message": "", - "cancel_subscription_with_addon_message": "", - "subscription_cancel_success": "", - "reactivate_subscription": "", - "reactivate_subscription_message": "", - "subscription_activate_success": "", - "thank_you": "", - "cancel_subscription_on_mobile": "", - "cancel_subscription_on_mobile_message": "", - "mail_to_manage_subscription": "", - "rename": "", + "manage_family": "Aileyi yönet", + "family_plan": "Aile Planı", + "leave_family_plan": "Aile planından ayrıl", + "leave": "Ayrıl", + "leave_family_plan_confirm": "Aile planından ayrılmak istediğinize emin misiniz?", + "choose_plan": "Planınızı seçin", + "manage_plan": "Aboneliği yönet", + "current_usage": "Mevcut kullanım {{usage}}", + "two_months_free": "Yıllık planlarda 2 ay ücretsiz", + "free_plan_option": "Ücretsiz planla devam et", + "free_plan_description": "{{storage}} ömür boyu ücretsiz", + "active": "Aktif", + "subscription_info_free": "Ücretsiz planı kullanıyorsunuz", + "subscription_info_family": "Aile planınız şu kişi tarafından yönetiliyor", + "subscription_info_expired": "Aboneliğinizin süresi doldu, lütfen yenileyin", + "subscription_info_renewal_cancelled": "Aboneliğiniz şu tarihte iptal edilecek: {{date, date}}", + "subscription_info_storage_quota_exceeded": "Depolama kotanızı aştınız, lütfen yükseltin", + "subscription_status_renewal_active": "Yenileme tarihi: {{date, date}}", + "subscription_status_renewal_cancelled": "Bitiş tarihi: {{date, date}}", + "add_on_valid_till": "{{storage}} eklentiniz {{date, date}} tarihine kadar geçerli", + "subscription_expired": "Abonelik süresi doldu", + "storage_quota_exceeded": "Depolama sınırı aşıldı", + "subscription_purchase_success": "

Ödemenizi aldık

Aboneliğiniz {{date, date}} tarihine kadar geçerlidir

", + "subscription_purchase_cancelled": "Satın alma iptal edildi, abone olmak istiyorsanız lütfen tekrar deneyin", + "subscription_purchase_failed": "Abonelik satın alma işlemi başarısız oldu, lütfen tekrar deneyin", + "subscription_verification_error": "Abonelik doğrulaması başarısız oldu", + "update_payment_method_message": "Üzgünüz, kartınızdan ücret alınırken ödeme başarısız oldu. Lütfen ödeme yöntemini güncelleyin ve tekrar deneyin", + "payment_method_authentication_failed": "Ödeme yöntemi doğrulanamadı. Lütfen farklı bir ödeme yöntemi seçin ve tekrar deneyin", + "update_payment_method": "Ödeme yöntemini güncelle", + "monthly": "Aylık", + "yearly": "Yıllık", + "month_short": "ay", + "year": "yıl", + "update_subscription": "Planı değiştir", + "update_subscription_title": "Plan değişikliğini onaylayın", + "update_subscription_message": "Planı değiştirmek istediğinize emin misiniz?", + "cancel_subscription": "Aboneliği iptal et", + "cancel_subscription_message": "

Tüm verileriniz bu faturalama döneminin sonunda sunucularımızdan silinecektir.

Aboneliğinizi iptal etmek istediğinizden emin misiniz?

", + "cancel_subscription_with_addon_message": "

Aboneliğinizi iptal etmek istediğinizden emin misiniz?

", + "subscription_cancel_success": "Abonelik başarıyla iptal edildi", + "reactivate_subscription": "Aboneliği Yenileyin", + "reactivate_subscription_message": "Yeniden etkinleştirildiğinde, {{date, date}} tarihinde faturalandırılacaksınız", + "subscription_activate_success": "Abonelik başarıyla etkinleştirildi ", + "thank_you": "Teşekkürler", + "cancel_subscription_on_mobile": "Mobil aboneliği iptal et", + "cancel_subscription_on_mobile_message": "Aboneliği burada etkinleştirmek için lütfen mobil uygulama üzerinden iptal edin", + "mail_to_manage_subscription": "Aboneliğinizi yönetmek için lütfen {{emailID}} adresinden bizimle iletişime geçin", + "rename": "Yeniden adlandır", "rename_file": "Dosyayı yeniden adlandır", "rename_album": "Albümü yeniden adlandır", "delete_album": "Albümü sil", "delete_album_title": "Albüm silinsin mi?", - "delete_album_message": "Ayrıca bu albümde bulunan fotoğrafları (ve videoları) bunların parçası olduğu diğer tüm albümlerden silmek mi istiyorsun?", + "delete_album_message": "Bu albümde bulunan fotoğrafları (ve videoları) yer aldıkları tüm diğer albümlerden de silmek istiyor musunuz?", "delete_photos": "Fotoğrafları sil", "keep_photos": "Fotoğrafları sakla", "share_album": "Albümü paylaş", - "sharing_with_self": "", - "sharing_already_shared": "", - "sharing_album_not_allowed": "", - "sharing_disabled_for_free_accounts": "", - "sharing_user_does_not_exist": "", - "search": "", - "search_results": "", - "no_results": "", - "search_hint": "", - "album": "", - "date": "", - "description": "", - "file_type": "", - "magic": "", - "photos_count_zero": "", - "photos_count_one": "", - "photos_count": "", - "terms_and_conditions": "", - "people": "", - "indexing_scheduled": "", - "indexing_photos": "", - "indexing_fetching": "", - "indexing_people": "", - "syncing_wait": "", - "people_empty_too_few": "", - "unnamed_person": "", - "add_a_name": "", - "new_person": "", - "add_name": "", - "rename_person": "", - "reset_person_confirm": "", - "reset_person_confirm_message": "", - "ignore": "", - "ignore_person_confirm": "", - "ignore_person_confirm_message": "", - "ignored": "", - "show_person": "", - "review_suggestions": "", - "saved_choices": "", - "discard_changes": "", - "discard_changes_confirm_message": "", - "people_suggestions_finding": "", - "people_suggestions_empty": "", - "info": "", + "sharing_with_self": "Kendinizle paylaşamazsınız", + "sharing_already_shared": "Bunu zaten {{email}} ile paylaşıyorsun", + "sharing_album_not_allowed": "Albüm paylaşımına izin verilmiyor", + "sharing_disabled_for_free_accounts": "Ücretsiz hesaplar için paylaşım devre dışıdır", + "sharing_user_does_not_exist": "Bu e-posta adresine sahip bir kullanıcı bulunamadı", + "search": "Ara", + "search_results": "Arama sonuçları", + "no_results": "Hiçbir sonuç bulunamadı", + "search_hint": "Albüm, tarih, açıklama ara, ...", + "album": "Albüm", + "date": "Tarih", + "description": "Açıklama", + "file_type": "Dosya türü", + "magic": "Sihir", + "photos_count_zero": "Anı yok", + "photos_count_one": "1 anı", + "photos_count": "{{count, number}} anı", + "terms_and_conditions": "Şartlar ve gizlilik politikasını kabul ediyorum", + "people": "Kişiler", + "indexing_scheduled": "Dizinleme planlandı...", + "indexing_photos": "Dizinler güncelleniyor...", + "indexing_fetching": "Dizinler eşitleniyor...", + "indexing_people": "Kişiler eşitleniyor...", + "syncing_wait": "Senkronize ediliyor...", + "people_empty_too_few": "Yeterli sayıda fotoğraf olduğunda kişiler burada görünecek", + "unnamed_person": "İsimsiz kişi", + "add_a_name": "İsim ekle", + "new_person": "Yeni kişi", + "add_name": "İsim Ekle", + "rename_person": "Kişiyi yeniden adlandır", + "reset_person_confirm": "Kişiyi sıfırla?", + "reset_person_confirm_message": "Bu kişi için ad, yüz grupları ve öneriler sıfırlanacak", + "ignore": "Yoksay", + "ignore_person_confirm": "Kişiyi yoksay?", + "ignore_person_confirm_message": "Bu yüz grubu kişi listesinde gösterilmeyecek", + "ignored": "Yoksayıldı", + "show_person": "Kişiyi Göster", + "review_suggestions": "Önerileri gözden geçir", + "saved_choices": "Değişiklikler kaydedildi", + "discard_changes": "Değişiklikleri iptal et", + "discard_changes_confirm_message": "Kaydedilmemiş değişiklikleriniz var. Kaydetmeden çıkarsanız hepsi kaybolacak", + "people_suggestions_finding": "Benzer yüzler bulunuyor...", + "people_suggestions_empty": "Şu anda başka öneri yok", + "info": "Bilgi", "file_name": "Dosya adı", - "caption_placeholder": "", - "location": "", - "view_on_map": "", - "map": "", - "enable_map": "", - "enable_maps_confirm": "", - "enable_maps_confirm_message": "", - "disable_map": "", - "disable_maps_confirm": "", - "disable_maps_confirm_message": "", - "details": "", - "view_exif": "", - "no_exif": "", - "exif": "", - "two_factor": "", - "two_factor_authentication": "", - "two_factor_qr_help": "", - "two_factor_manual_entry_title": "", - "two_factor_manual_entry_message": "", - "scan_qr_title": "", - "enable_two_factor": "", - "enable": "", - "enabled": "", - "lost_2fa_device": "", - "incorrect_code": "", - "two_factor_info": "", - "disable": "", - "reconfigure": "", - "reconfigure_two_factor_hint": "", - "update_two_factor": "", - "update_two_factor_message": "", - "update": "", - "disable_two_factor": "", - "disable_two_factor_message": "", + "caption_placeholder": "Açıklama ekle", + "location": "Konum", + "view_on_map": "OpenStreetMap'te görüntüle", + "map": "Harita", + "enable_map": "Haritayı aç", + "enable_maps_confirm": "Haritalar etkinleştirilsin mi?", + "enable_maps_confirm_message": "

Bu, fotoğraflarınızı dünya haritası üzerinde gösterecek.

Harita OpenStreetMap, tarafından barındırılır ve fotoğraflarınızın tam konumları asla paylaşılmaz.

Bu özelliği istediğiniz zaman Ayarlar'dan devre dışı bırakabilirsiniz.

", + "disable_map": "Haritayı kapat", + "disable_maps_confirm": "Haritalar devre dışı bırakılsın mı?", + "disable_maps_confirm_message": "

Bu işlem, fotoğraflarınızın dünya haritasında görüntülenmesini devre dışı bırakır.

Bu özelliği Ayarlar'dan dilediğiniz zaman yeniden etkinleştirebilirsiniz.

", + "details": "Ayrıntılar", + "view_exif": "Tüm Exif verilerini görüntüle", + "no_exif": "Exif verisi yok", + "exif": "Exif", + "two_factor": "İki faktör", + "two_factor_authentication": "İki faktörlü doğrulama", + "two_factor_qr_help": "Aşağıdaki QR kodunu tercih ettiğiniz kimlik doğrulayıcı uygulamayla tarayın", + "two_factor_manual_entry_title": "Kodu manuel olarak girin", + "two_factor_manual_entry_message": "Lütfen bu kodu favori kimlik doğrulayıcı uygulamanıza girin", + "scan_qr_title": "Bunun yerine QR kodu tarayın", + "enable_two_factor": "İki faktörlü kimlik doğrulamayı etkinleştir", + "enable": "Etkinleştir", + "enabled": "Etkinleştirildi", + "lost_2fa_device": "İki faktörlü cihazınızı mı kaybettiniz?", + "incorrect_code": "Yanlış kod", + "two_factor_info": "Hesabınıza giriş yapmak için yalnızca e-posta ve şifre dışında ek bir güvenlik katmanı ekleyin", + "disable": "Devre dışı", + "reconfigure": "Yeniden yapılandır", + "reconfigure_two_factor_hint": "Kimlik doğrulayıcı cihazınızı güncelleyin", + "update_two_factor": "İki faktörlü kimlik doğrulamayı güncelle", + "update_two_factor_message": "Devam etmeniz durumunda daha önce yapılandırılmış kimlik doğrulayıcılar geçersiz hale gelecektir", + "update": "Güncelle", + "disable_two_factor": "İki aşamalı doğrulamayı devre dışı bırak", + "disable_two_factor_message": "İki faktörlü kimlik doğrulamasını devre dışı bırakmak istediğinizden emin misiniz?", "export_data": "Verileri dışarı aktar", - "select_folder": "", - "select_zips": "", - "faq": "", - "takeout_hint": "", - "destination": "", - "start": "", - "last_export_time": "", - "export_again": "", - "local_storage_not_accessible": "", - "email_already_taken": "", - "live_photos_detected": "", - "ignored_uploads": "", - "ignored_uploads_hint": "", - "file_not_uploaded_list": "", - "failed_uploads": "", - "failed_uploads_hint": "", - "retry_failed_uploads": "", - "thumbnail_generation_failed": "", - "thumbnail_generation_failed_hint": "", - "unsupported_files": "", - "unsupported_files_hint": "", - "blocked_uploads": "", - "blocked_uploads_hint": "", - "large_files": "", - "large_files_hint": "", - "insufficient_storage": "", - "insufficient_storage_hint": "", - "uploads_in_progress": "", - "successful_uploads": "", - "upload_to_album": "", - "add_to_album": "", - "move_to_album": "", - "unhide_to_album": "", - "restore_to_album": "", - "section_all": "", - "section_uncategorized": "", - "section_archive": "", - "section_hidden": "", - "section_trash": "", - "favorites": "", - "archive": "", - "archive_album": "", - "unarchive": "", - "unarchive_album": "", - "hide_collection": "", - "unhide_collection": "", - "move": "", - "add": "", - "remove": "", - "yes_remove": "", - "remove_from_album": "", - "move_to_trash": "", - "trash_files_message": "", - "trash_file_message": "", - "delete_permanently": "", - "restore": "", - "empty_trash": "", - "empty_trash_title": "", - "empty_trash_message": "", - "leave_album": "", - "leave_shared_album_title": "", - "leave_shared_album_message": "", - "leave_shared_album": "", - "confirm_remove_message": "", - "confirm_remove_incl_others_message": "", - "oldest": "", - "last_updated": "", - "name": "", - "fix_creation_time": "", - "fix_creation_time_in_progress": "", - "fix_creation_time_file_updated": "", - "fix_creation_time_completed": "", - "fix_creation_time_completed_with_errors": "", - "fix_creation_time_options": "", - "exif_date_time_original": "", - "exif_date_time_digitized": "", - "exif_metadata_date": "", - "custom_time": "", - "caption_character_limit": "", - "sharing_details": "", - "modify_sharing": "", - "add_collaborators": "", - "add_new_email": "", - "shared_with_people_count_zero": "", - "shared_with_people_count_one": "", - "shared_with_people_count": "", - "participants_count_zero": "", - "participants_count_one": "", - "participants_count": "", - "add_viewers": "", - "change_permission_to_viewer": "", - "change_permission_to_collaborator": "", - "change_permission_title": "", - "confirm_convert_to_viewer": "", - "confirm_convert_to_collaborator": "", - "manage": "", - "added_as": "", - "collaborator_hint": "", - "remove_participant": "", - "remove_participant_title": "", - "remove_participant_message": "", - "confirm_remove": "", - "owner": "", - "collaborators": "", - "viewers": "", - "add_more": "", - "or_add_existing": "", - "not_found": "", - "link_expired": "", - "link_expired_message": "", - "manage_link": "", - "link_request_limit_exceeded": "", - "allow_downloads": "", - "allow_adding_photos": "", - "allow_adding_photos_hint": "", - "device_limit": "", - "none": "", - "link_expiry": "", - "never": "", + "select_folder": "Klasör seç", + "select_zips": "Dosya seç", + "faq": "Sık Sorulan Sorular", + "takeout_hint": "Tüm zip dosyalarını aynı klasöre çıkarıp o klasörü yükleyin. Veya zip dosyalarını doğrudan yükleyin. Ayrıntılar için SSS’ye bakın.", + "destination": "Varış noktası", + "start": "Başlat", + "last_export_time": "Son dışa aktarma zamanı", + "export_again": "Tekrar senkronize et", + "local_storage_not_accessible": "Tarayıcınız veya bir eklenti, Ente’nin verileri yerel depolamaya kaydetmesini engelliyor", + "email_already_taken": "Bu e-posta adresi zaten kullanılıyor", + "live_photos_detected": "Live Photos’taki fotoğraf ve video dosyalarınız tek bir dosyada birleştirildi", + "ignored_uploads": "Yoksayılan yüklemeler", + "ignored_uploads_hint": "Bu dosyalar atlandı çünkü aynı albümde isim ve içerik olarak eşleşen dosyalar bulundu", + "file_not_uploaded_list": "Aşağıdaki dosyalar yüklenmedi", + "failed_uploads": "Başarısız yüklemeler", + "failed_uploads_hint": "Yükleme tamamlandığında bu dosyalar için yeniden deneme seçeneği sunulacak", + "retry_failed_uploads": "Başarısız yüklemeleri yeniden dene", + "thumbnail_generation_failed": "Küçük resim oluşturulamadı", + "thumbnail_generation_failed_hint": "Bu dosyalar yüklendi, ancak ne yazık ki küçük resimleri oluşturulamadı.", + "unsupported_files": "Desteklenmeyen dosyalar", + "unsupported_files_hint": "Ente bu dosya formatlarını henüz desteklemiyor", + "blocked_uploads": "Engellenen yüklemeler", + "blocked_uploads_hint": "Tarayıcınız veya bir eklenti, Ente’nin büyük dosyaları yüklemek için eTags kullanmasını engelliyor.", + "large_files": "Büyük dosyalar", + "large_files_hint": "Bu dosyalar, maksimum dosya boyutu sınırını aştığı için yüklenmedi", + "insufficient_storage": "Yetersiz depolama alanı", + "insufficient_storage_hint": "Bu dosyalar, depolama planınızın maksimum boyut sınırını aştığı için yüklenmedi", + "uploads_in_progress": "Devam eden yüklemeler", + "successful_uploads": "Başarılı yüklemeler", + "upload_to_album": "Albüme yükle", + "add_to_album": "Albüme ekle", + "move_to_album": "Albüme taşı", + "unhide_to_album": "Albümü göster", + "restore_to_album": "Albümü yenile", + "section_all": "Tümü", + "section_uncategorized": "Kategorize edilmemiş", + "section_archive": "Arşiv", + "section_hidden": "Gizle", + "section_trash": "Çöp", + "favorites": "Favoriler", + "archive": "Arşiv", + "archive_album": "Albümü arşivle", + "unarchive": "Arşivden çıkar", + "unarchive_album": "Albümden çıkar", + "hide_collection": "Albümü gizle", + "unhide_collection": "Albümü göster", + "move": "Taşı", + "add": "Ekle", + "remove": "Kaldır", + "yes_remove": "Evet, kaldır", + "remove_from_album": "Albümden kaldır", + "move_to_trash": "Çöp Kutusuna taşı", + "trash_files_message": "Seçilen dosyalar tüm albümlerden silinecek ve çöp kutusuna taşınacak.", + "trash_file_message": "Seçilen dosya tüm albümlerden silinecek ve çöp kutusuna taşınacak.", + "delete_permanently": "Kalıcı sil", + "restore": "Geri yükle", + "empty_trash": "Çöp kutusunu boşalt", + "empty_trash_title": "Çöp kutusu boşaltılsın mı?", + "empty_trash_message": "Bu dosyalar Ente hesabınızdan kalıcı olarak silinecek.", + "leave_album": "Albümden ayrıl", + "leave_shared_album_title": "Paylaşılan albümden çıkılsın mı?", + "leave_shared_album_message": "Albümden çıkacaksınız ve albüm size artık görünmeyecek.", + "leave_shared_album": "Evet, ayrıl", + "confirm_remove_message": "Seçilen öğeler bu albümden kaldırılacak. Sadece bu albümde bulunan öğeler \"Sınıflandırılmamış\" kategorisine taşınacak.", + "confirm_remove_incl_others_message": "Kaldırdığınız öğelerden bazıları başkaları tarafından eklenmiştir ve bunlara erişiminizi kaybedeceksiniz.", + "oldest": "En eski", + "last_updated": "Son Güncellenen", + "name": "İsim", + "fix_creation_time": "Zamanı düzelt", + "fix_creation_time_in_progress": "Zaman düzeltiliyor", + "fix_creation_time_file_updated": "Dosya zamanı güncellendi", + "fix_creation_time_completed": "Tüm dosyalar başarıyla güncellendi", + "fix_creation_time_completed_with_errors": "Bazı dosyalar için zaman güncellemesi başarısız oldu, lütfen tekrar deneyin", + "fix_creation_time_options": "Kullanmak istediğiniz seçeneği seçin", + "exif_date_time_original": "Exif:Orijinal Tarih-Saat", + "exif_date_time_digitized": "Exif:Dijital Tarih-Saat", + "exif_metadata_date": "Exif:Metadata Tarihi", + "custom_time": "Özel zaman", + "caption_character_limit": "Maksimum 5000 karakter", + "sharing_details": "Paylaşım ayrıntıları", + "modify_sharing": "Paylaşımı düzenle", + "add_collaborators": "Katılımcı ekle", + "add_new_email": "Yeni bir e-posta ekle", + "shared_with_people_count_zero": "Belirli kişilerle paylaş", + "shared_with_people_count_one": "1 kişiyle paylaşıldı", + "shared_with_people_count": "{{count, number}} kişiyle paylaşıldı", + "participants_count_zero": "Katılımcı yok", + "participants_count_one": "1 katılımcı", + "participants_count": "{{count, number}} katılımcı", + "add_viewers": "Görüntüleyici ekle", + "change_permission_to_viewer": "

{{selectedEmail}} albüme daha fazla fotoğraf ekleyemeyecek

Ancak kendi eklediği fotoğrafları kaldırabilecek

", + "change_permission_to_collaborator": "{{selectedEmail}} albüme fotoğraf ekleyebilecek", + "change_permission_title": "İzinleri değiştir?", + "confirm_convert_to_viewer": "Evet, yalnızca görüntüleyici yap", + "confirm_convert_to_collaborator": "Evet, işbirlikçi yap", + "manage": "Yönet", + "added_as": "Eklendi", + "collaborator_hint": "İşbirlikçiler paylaşılan albüme fotoğraf ve video ekleyebilir", + "remove_participant": "Katılımcıyı kaldır", + "remove_participant_title": "Kaldırılsın mı?", + "remove_participant_message": "

{{selectedEmail}} albümden kaldırılacak

Ekledikleri tüm fotoğraflar da albümden silinecek

", + "confirm_remove": "Evet, kaldır", + "owner": "Sahip", + "collaborators": "İşbirlikçiler", + "viewers": "Görüntüleyenler", + "add_more": "Daha fazla ekle", + "or_add_existing": "Ya da var olan birini seçin", + "not_found": "404 - Bulunamadı", + "link_expired": "Bağlantının süresi doldu", + "link_expired_message": "Bu bağlantının süresi dolmuş ya da devre dışı bırakılmış", + "manage_link": "Linki yönet", + "link_request_limit_exceeded": "Bu albüm çok fazla cihazda görüntülendi", + "allow_downloads": "İndirmelere izin ver", + "allow_adding_photos": "Fotoğraf eklemeye izin ver", + "allow_adding_photos_hint": "Bağlantıya sahip kişilerin de paylaşılan albüme fotoğraf eklemesine izin ver.", + "device_limit": "Cihaz limiti", + "none": "Boş", + "link_expiry": "Bağlantı süresi sonu", + "never": "Asla", "after_time": { - "hour": "", - "day": "", - "week": "", - "month": "", - "year": "" + "hour": "Bir saat sonra", + "day": "Bir gün sonra", + "week": "Bir hafta sonra", + "month": "Bir ay sonra", + "year": "Bir yıl sonra" }, - "copy_link": "", - "done": "", - "share_link_section_title": "", - "remove_link": "", - "create_public_link": "", - "public_link_created": "", - "public_link_enabled": "", - "collect_photos": "", - "disable_file_download": "", - "disable_file_download_message": "", - "shared_using": "", - "sharing_referral_code": "", - "disable_password": "", - "disable_password_message": "", - "password_lock": "", - "lock": "", - "file": "", - "folder": "", - "google_takeout": "", - "deduplicate_files": "", - "remove_duplicates": "", - "total_size": "", - "count": "", - "deselect_all": "", - "no_duplicates": "", - "duplicate_group_description": "", - "remove_duplicates_button_count": "", - "stop_uploads_title": "", - "stop_uploads_message": "", - "yes_stop_uploads": "", - "stop_downloads_title": "", - "stop_downloads_message": "", - "yes_stop_downloads": "", - "albums": "", - "albums_count_one": "", - "albums_count": "", - "all_albums": "", - "all_hidden_albums": "", - "hidden_albums": "", - "hidden_items": "", - "enter_two_factor_otp": "", - "create_account": "", - "copied": "", - "upgrade_now": "", - "renew_now": "", - "storage": "", - "used": "", - "you": "", - "family": "", - "free": "", - "of": "", - "watch_folders": "", - "watched_folders": "", - "no_folders_added": "", - "watch_folders_hint_1": "", - "watch_folders_hint_2": "", - "watch_folders_hint_3": "", - "add_folder": "", - "stop_watching": "", - "stop_watching_folder_title": "", - "stop_watching_folder_message": "", - "yes_stop": "", - "change_folder": "", - "view_logs": "", - "view_logs_message": "", - "weak_device_hint": "", - "drag_and_drop_hint": "", - "authenticate": "", - "uploaded_to_single_collection": "", - "uploaded_to_separate_collections": "", - "nevermind": "", - "update_available": "", - "update_installable_message": "", - "install_now": "", - "install_on_next_launch": "", - "update_available_message": "", - "download_and_install": "", - "ignore_this_version": "", - "today": "", - "yesterday": "", + "copy_link": "Bağlantıyı kopyala", + "done": "Tamamlandı", + "share_link_section_title": "Ya da bir bağlantı paylaş", + "remove_link": "Bağlantıyı kaldır", + "create_public_link": "Herkese açık bir bağlantı oluştur", + "public_link_created": "Herkese açık bağlantı oluşturuldu", + "public_link_enabled": "Herkese açık bağlantı aktive edildi", + "collect_photos": "Fotoğraf seç", + "disable_file_download": "İndirme seçeneğini devre dışı bırak", + "disable_file_download_message": "

Dosyalar için indirme butonunu devre dışı bırakmak istediğinizden emin misiniz?

Görüntüleyiciler yine de ekran görüntüsü alabilir veya harici araçlarla fotoğraflarınızı kaydedebilir.

", + "shared_using": "{{url}} üzerinden paylaşıldı", + "sharing_referral_code": "{{referralCode}} kodunu kullanarak 10 GB ücretsiz depolama kazanın", + "disable_password": "Parola kilidini devre dışı bırak", + "disable_password_message": "Parola kilidini devre dışı bırakmak istediğinizden emin misiniz?", + "password_lock": "Parola kilidi", + "lock": "Kilitle", + "file": "Dosya", + "folder": "Klasör", + "google_takeout": "Google takeout", + "deduplicate_files": "Yinelenen dosyaları kaldır", + "remove_duplicates": "Yinelenenleri kaldır", + "total_size": "Toplam boyut", + "count": "Sayı", + "deselect_all": "Tüm seçimleri kaldır", + "no_duplicates": "Yinelenen yok", + "duplicate_group_description": "{{count}} öğe, her biri {{itemSize}}", + "remove_duplicates_button_count": "{{count, number}} öğeyi sil", + "stop_uploads_title": "Yüklemeyi durdur?", + "stop_uploads_message": "Devam eden tüm yüklemeleri durdurmak istediğinizden emin misiniz?", + "yes_stop_uploads": "Evet, yüklemeyi durdur", + "stop_downloads_title": "İndirmeyi durdur?", + "stop_downloads_message": "Devam eden tüm indirmeleri durdurmak istediğinizden emin misiniz?", + "yes_stop_downloads": "Evet, indirmeyi durdur", + "albums": "Albümler", + "albums_count_one": "1 Albüm", + "albums_count": "{{count, number}} Albüm", + "all_albums": "Tüm albümler", + "all_hidden_albums": "Tüm gizli albümler", + "hidden_albums": "Gizli albümler", + "hidden_items": "Gizli öğeler", + "enter_two_factor_otp": "Kimlik doğrulama uygulamanızdaki 6 haneli kodu girin.", + "create_account": "Hesap oluştur", + "copied": "Kopyalandı", + "upgrade_now": "Şimdi yükselt", + "renew_now": "Şimdi yenile", + "storage": "Depolama", + "used": "kullanıldı", + "you": "Siz", + "family": "Aile", + "free": "ücretsiz", + "of": "/", + "watch_folders": "Klasörleri görüntüle", + "watched_folders": "Görüntülenen klasörler", + "no_folders_added": "Henüz klasör eklenmedi", + "watch_folders_hint_1": "Buraya eklediğiniz klasörler otomatik olarak görüntülenecek", + "watch_folders_hint_2": "Yeni dosyaları Ente'ye yükle", + "watch_folders_hint_3": "Silinen dosyaları Ente'den kaldır", + "add_folder": "Klasör ekle", + "stop_watching": "İzlemeyi durdur", + "stop_watching_folder_title": "Bu klasörü görüntülemeyi bırakmak istiyor musunuz?", + "stop_watching_folder_message": "Mevcut dosyalarınız silinmeyecek, ancak Ente bu klasördeki değişikliklerde bağlantılı Ente albümünü otomatik olarak güncellemeyi durduracaktır.", + "yes_stop": "Evet, durdur", + "change_folder": "Klasörü değiştir", + "view_logs": "Kayıtları görüntüle", + "view_logs_message": "

Bu, yaşadığınız sorunu inceleyebilmemiz için bize e-posta ile gönderebileceğiniz hata ayıklama kayıtlarını gösterir.

Belirli dosyalarla ilgili sorunları takip edebilmemiz için dosya adları da dahil edilecektir.

", + "weak_device_hint": "Kullandığınız web tarayıcısı, fotoğraflarınızı şifrelemek için yeterince güçlü değil. Lütfen bilgisayarınızdan Ente'ye giriş yapın veya Ente'nin mobil/masaüstü uygulamasını indirin.", + "drag_and_drop_hint": "Ya da dosyaları Ente penceresine sürükleyip bırakın", + "authenticate": "Doğrula", + "uploaded_to_single_collection": "Tek bir koleksiyona yüklendi", + "uploaded_to_separate_collections": "Ayrı koleksiyonlara yüklendi", + "nevermind": "Boşver", + "update_available": "Güncelleme mevcut", + "update_installable_message": "Ente'nin yeni bir sürümü yüklenmeye hazır.", + "install_now": "Şimdi yükle", + "install_on_next_launch": "Bir sonraki başlatmada yükle", + "update_available_message": "Ente'nin yeni bir sürümü yayınlandı ancak otomatik olarak indirilemez ve yüklenemez.", + "download_and_install": "İndir ve kur", + "ignore_this_version": "Bu sürümü yoksay", + "today": "Bugün", + "yesterday": "Dün", "enter_name": "İsim gir", - "uploader_name_hint": "Arkadaşlarının bu harika fotoğraflar için kime teşekkür etmeleri gerektiğini bilmeleri için bir isim ekle!", - "name_placeholder": "", - "more_details": "", - "ml_search": "", - "ml_search_description": "", - "ml_search_footnote": "", - "indexing": "", - "processed": "", - "indexing_status_running": "", - "indexing_status_fetching": "", - "indexing_status_scheduled": "", - "indexing_status_done": "", - "ml_search_disable": "", - "ml_search_disable_confirm": "", - "ml_consent": "", - "ml_consent_title": "", - "ml_consent_description": "", - "ml_consent_confirmation": "", - "labs": "", - "passphrase_strength_weak": "", - "passphrase_strength_moderate": "", - "passphrase_strength_strong": "", - "preferences": "", - "language": "", - "advanced": "", - "export_directory_does_not_exist": "", - "export_directory_does_not_exist_message": "", + "uploader_name_hint": "Arkadaşlarının bu harika fotoğraflar için kime teşekkür etmeleri gerektiğini bilmeleri için bir ad ekleyin!", + "name_placeholder": "İsim...", + "more_details": "Daha fazla", + "ml_search": "Makine öğrenimi", + "ml_search_description": "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde çalışan makine öğrenimini kullanır", + "ml_search_footnote": "Sihirli arama fotoğrafları içeriğine göre aramanızı sağlar; örneğin: 'araba', 'kırmızı araba', 'Ferrari'", + "indexing": "Dizine ekleniyor", + "processed": "İşlenen", + "indexing_status_running": "Çalışıyor", + "indexing_status_fetching": "Alınıyor", + "indexing_status_scheduled": "Planlandı", + "indexing_status_done": "Tamamlandı", + "ml_search_disable": "Makine öğrenimini kapat", + "ml_search_disable_confirm": "Tüm cihazlarınızda makine öğrenimini devre dışı bırakmak istiyor musunuz?", + "ml_consent": "Makine öğrenimini etkinleştir", + "ml_consent_title": "Makine öğrenimi etkinleştirilsin mi?", + "ml_consent_description": "

Makine öğrenimini etkinleştirirseniz, Ente dosyalardan yüz geometrisi gibi bilgiler çıkaracaktır, buna sizinle paylaşılanlar da dahildir.

Bu işlemler cihazınızda gerçekleşir ve oluşturulan tüm biyometrik veriler uçtan uca şifrelenir.

Bu özellikle ilgili daha fazla bilgi için lütfen gizlilik politikamıza göz atın.

", + "ml_consent_confirmation": "Anladım ve makine öğrenimini etkinleştirmek istiyorum", + "labs": "Deneysel özellikler", + "password_strength_weak": "Parola gücü: Zayıf", + "password_strength_moderate": "Parola gücü: Orta", + "password_strength_strong": "Parola gücü: Güçlü", + "preferences": "Tercihler", + "language": "Dil", + "advanced": "Gelişmiş", + "export_directory_does_not_exist": "Geçersiz dışa aktarma dizini", + "export_directory_does_not_exist_message": "

Seçtiğiniz dışa aktarma dizini mevcut değil.

Lütfen geçerli bir dizin seçin.

", "storage_unit": { - "b": "", - "kb": "", - "mb": "", - "gb": "", - "tb": "" + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, - "stop": "", - "sync_continuously": "", - "export_starting": "", - "export_preparing": "", - "export_renaming_album_folders": "", - "export_trashing_deleted_files": "", - "export_trashing_deleted_albums": "", - "export_progress": "", - "pending_items": "", - "delete_account_reason_label": "", - "delete_account_reason_placeholder": "", + "stop": "Durdur", + "sync_continuously": "Sürekli senkronize et", + "export_starting": "Dışa aktarım başlatılıyor...", + "export_preparing": "Hazırlanıyor...", + "export_renaming_album_folders": "Albüm klasörleri yeniden adlandırılıyor...", + "export_trashing_deleted_files": "Silinen dosyalar çöpe taşınıyor...", + "export_trashing_deleted_albums": "Silinen albümler çöpe taşınıyor...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} öğe senkronize edildi", + "pending_items": "Bekleyen öğeler", + "delete_account_reason_label": "Hesabınızı silme sebebiniz nedir?", + "delete_account_reason_placeholder": "Bir neden seçin", "delete_reason": { - "missing_feature": "", - "behaviour": "", - "found_another_service": "", - "not_listed": "" + "missing_feature": "İhtiyacım olan önemli bir özellik eksik", + "behaviour": "Uygulama veya bir özellik, düşündüğüm şekilde çalışmıyor", + "found_another_service": "Daha iyi başka bir hizmet buldum", + "not_listed": "Nedenim listede yok" }, - "delete_account_feedback_label": "", - "delete_account_feedback_placeholder": "", - "delete_account_confirm_checkbox_label": "", - "delete_account_confirm": "", - "delete_account_confirm_message": "", - "feedback_required": "", - "feedback_required_found_another_service": "", - "recover_two_factor": "", - "at": "", - "auth_next": "", - "auth_download_mobile_app": "", - "no_codes_added_yet": "", - "hide": "", - "unhide": "", - "sort_by": "", - "newest_first": "", - "oldest_first": "", - "pin_album": "", - "unpin_album": "", - "unpreviewable_file_message": "", - "download_complete": "", - "downloading_album": "", - "download_failed": "", - "download_progress": "", - "christmas": "", - "christmas_eve": "", - "new_year": "", - "new_year_eve": "", - "image": "", - "video": "", - "live_photo": "", - "live": "", - "edit_image": "", - "photo_editor": "", - "confirm_editor_close": "", - "confirm_editor_close_message": "", - "brightness": "", - "contrast": "", - "saturation": "", - "blur": "", - "transform": "", - "crop": "", - "aspect_ratio": "", - "square": "", - "freehand": "", - "apply_crop": "", - "rotation": "", - "rotate_left": "", - "rotate_right": "", - "flip": "", - "flip_vertically": "", - "flip_horizontally": "", - "download_edited": "", - "save_a_copy_to_ente": "", - "restore_original": "", - "photo_edit_required_to_save": "", - "colors": "", - "invert_colors": "", - "reset": "", - "faster_upload": "", - "faster_upload_description": "", - "open_ente_on_startup": "", - "cast_album_to_tv": "", - "enter_cast_pin_code": "", - "code": "", - "pair_device_to_tv": "", - "tv_not_found": "", - "cast_auto_pair": "", - "cast_auto_pair_description": "", - "choose_device_from_browser": "", - "cast_auto_pair_failed": "", - "pair_with_pin": "", - "pair_with_pin_description": "", - "visit_cast_url": "", - "passkeys": "", - "passkey_fetch_failed": "", - "manage_passkey": "", - "delete_passkey": "", - "delete_passkey_confirmation": "", - "rename_passkey": "", - "add_passkey": "", - "enter_passkey_name": "", - "passkeys_description": "", - "created_at": "", - "passkey_add_failed": "", - "passkey_login_failed": "", - "passkey_login_invalid_url": "", - "passkey_login_already_claimed_session": "", - "passkey_login_generic_error": "", - "passkey_login_credential_hint": "", - "passkeys_not_supported": "", - "try_again": "", - "check_status": "", - "passkey_login_instructions": "", - "passkey_login": "", - "totp_login": "", - "passkey": "", - "passkey_verify_description": "", - "waiting_for_verification": "", - "verification_still_pending": "", - "passkey_verified": "", - "redirecting_back_to_app": "", - "redirect_close_instructions": "", - "redirect_again": "", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "", - "developer_settings": "", - "server_endpoint": "", - "more_information": "", - "save": "", - "theme": "", - "system": "", - "light": "", - "dark": "" + "delete_account_feedback_label": "Gittiğini gördüğümüze üzüldük. Lütfen gelişmemize yardımcı olmak için neden ayrıldığınızı açıklayın.", + "delete_account_feedback_placeholder": "Geri bildirim", + "delete_account_confirm_checkbox_label": "Evet, bu hesabı ve tüm verileri kalıcı olarak silmek istiyorum", + "delete_account_confirm": "Hesap silme işlemini onayla", + "delete_account_confirm_message": "

Bu hesap, kullanıyorsanız diğer Ente uygulamalarıyla bağlantılıdır.

Tüm Ente uygulamalarında yüklediğiniz verilerin silinmesi planlanacak ve hesabınız kalıcı olarak silinecektir.

", + "feedback_required": "Lütfen bu bilgilerle bize yardımcı olun", + "feedback_required_found_another_service": "Diğer hizmetin neyi daha iyi yaptığını açıklar mısınız?", + "recover_two_factor": "İki faktörlü doğrulamayı kurtar", + "at": "de", + "auth_next": "sonraki", + "auth_download_mobile_app": "Bilgilerinizi yönetmek için mobil uygulamamızı indirin", + "no_codes_added_yet": "Henüz kod eklenmedi", + "hide": "Gizle", + "unhide": "Göster", + "sort_by": "Sırala", + "newest_first": "Yeniden eskiye", + "oldest_first": "Eskiden yeniye", + "pin_album": "Albümü sabitle", + "unpin_album": "Albümün sabitlemesini kaldır", + "unpreviewable_file_message": "Bu dosya önizlenemedi", + "download_complete": "İndirme tamamlandı", + "downloading_album": "{{name}} İndiriliyor", + "download_failed": "İndirme başarısız", + "download_progress": "{{count, number}} / {{total, number}}", + "christmas": "Noel", + "christmas_eve": "Noel Arifesi", + "new_year": "Yeni yıl", + "new_year_eve": "Yılbaşı Gecesi", + "image": "Görsel", + "video": "Video", + "live_photo": "Canlı Fotoğraf", + "live": "Canlı", + "edit_image": "Görseli düzenle", + "photo_editor": "Fotoğraf düzenleyici", + "confirm_editor_close": "Editörü kapatmak istediğinizden emin misiniz?", + "confirm_editor_close_message": "Düzenlediğiniz görseli indirin veya değişikliklerinizi kalıcı hale getirmek için bir kopyasını Ente'ye kaydedin.", + "brightness": "Parlaklık", + "contrast": "Kontrast", + "saturation": "Doygunluk", + "blur": "Bulanıklık", + "transform": "Dönüştür", + "crop": "Kırp", + "aspect_ratio": "En boy oranı", + "square": "Kare", + "freehand": "Serbest", + "apply_crop": "Kırpmayı uygula", + "rotation": "Yön", + "rotate_left": "Sola döndür", + "rotate_right": "Sağa döndür", + "flip": "Ters çevir", + "flip_vertically": "Dikey çevir", + "flip_horizontally": "Yatay çevir", + "download_edited": "Düzenlenmişi indir", + "save_a_copy_to_ente": "Bir kopyasını Ente’ye kaydet", + "restore_original": "Orijinali geri yükle", + "photo_edit_required_to_save": "Kaydetmeden önce en az bir dönüşüm veya renk ayarı yapılmalıdır.", + "colors": "Renkler", + "invert_colors": "Renkleri ters çevir", + "reset": "Sıfırla", + "faster_upload": "Daha hızlı yükleme", + "faster_upload_description": "Yüklemeleri yakındaki sunucular üzerinden yönlendir", + "open_ente_on_startup": "Başlangıçta Ente’yi aç", + "cast_album_to_tv": "Albümü TV'de oynat", + "cast_to_tv": "", + "enter_cast_pin_code": "Bu cihazı eşleştirmek için TV ekranında gördüğünüz kodu girin.", + "code": "Kod", + "pair_device_to_tv": "Cihazları eşleştir", + "tv_not_found": "TV bulunamadı. PIN kodunu doğru girdiğinizden emin misiniz?", + "cast_auto_pair": "Otomatik eşle", + "cast_auto_pair_description": "Otomatik eşleştirme yalnızca Chromecast destekli cihazlarla çalışır.", + "choose_device_from_browser": "Tarayıcı penceresinden yayın destekli bir cihaz seçin.", + "cast_auto_pair_failed": "Chromecast otomatik eşleştirme başarısız oldu. Lütfen tekrar deneyin.", + "pair_with_pin": "PIN ile eşle", + "pair_with_pin_description": "PIN ile eşleştirme, albümünüzü görüntülemek istediğiniz tüm ekranlarla çalışır.", + "visit_cast_url": "Albümünüzü görüntülemek istediğiniz cihazda {{url}} adresini ziyaret edin.", + "passkeys": "Geçiş anahtarları", + "passkey_fetch_failed": "Parola anahtarlarınız alınamadı.", + "manage_passkey": "Geçiş anahtarlarını yönet", + "delete_passkey": "Geçiş anahtarını sil", + "delete_passkey_confirmation": "Bu parola anahtarını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "rename_passkey": "Parola anahtarını yeniden adlandır", + "add_passkey": "Geçiş anahtarı ekle", + "enter_passkey_name": "Parola anahtarı adını girin", + "passkeys_description": "Parola anahtarları, Ente hesabınız için modern ve güvenli bir ikinci doğrulama yöntemidir. Kullanıcı kolaylığı ve güvenlik için cihazdaki biyometrik kimlik doğrulamayı kullanırlar.", + "created_at": "Oluşturuldu", + "passkey_add_failed": "Parola anahtarı eklenemedi", + "passkey_login_failed": "Parola anahtarıyla giriş başarısız oldu", + "passkey_login_invalid_url": "Giriş URL'si geçersiz.", + "passkey_login_already_claimed_session": "Bu oturum zaten doğrulandı.", + "passkey_login_generic_error": "Parola ile oturum açarken bir hata oluştu.", + "passkey_login_credential_hint": "Parolanız farklı bir aygıttaysa, doğrulamak için bu sayfayı o aygıtta açın.", + "passkeys_not_supported": "Geçiş anahtarları bu tarayıcıda desteklenmiyor", + "try_again": "Tekrar deneyin", + "check_status": "Durumu kontrol et", + "passkey_login_instructions": "Giriş yapmaya devam etmek için tarayıcınızdaki adımları takip edin.", + "passkey_login": "Parola anahtarıyla giriş yap", + "totp_login": "TOTP ile giriş yap", + "passkey": "Geçiş anahtarı", + "passkey_verify_description": "Hesabınıza giriş yapmak için parola anahtarınızı doğrulayın.", + "waiting_for_verification": "Doğrulama bekleniyor...", + "verification_still_pending": "Doğrulama hala bekliyor", + "passkey_verified": "Geçiş anahtarı doğrulandı", + "redirecting_back_to_app": "Uygulamaya geri yönlendiriliyorsunuz...", + "redirect_close_instructions": "Uygulama açıldıktan sonra bu pencereyi kapatabilirsiniz.", + "redirect_again": "Yeniden yönlendir", + "autogenerated_first_album_name": "İlk Albümüm", + "autogenerated_default_album_name": "Yeni albüm", + "developer_settings": "Geliştirici ayarları", + "server_endpoint": "Sunucu uç noktası", + "more_information": "Daha fazla bilgi", + "save": "Kaydet", + "theme": "Tema", + "system": "Sistem", + "light": "Aydınlık", + "dark": "Karanlık", + "streamable_videos": "Yayınlanabilir videolar", + "processing_videos_status": "Videolar işleniyor...", + "share_favorites": "Favorileri paylaş", + "person_favorites": "{{name}}'in favorileri", + "shared_favorites": "Paylaşılan favoriler", + "added_by_name": "{{name}} tarafından eklendi" } diff --git a/web/packages/base/locales/uk-UA/translation.json b/web/packages/base/locales/uk-UA/translation.json index 4005af0e72..38b56509b3 100644 --- a/web/packages/base/locales/uk-UA/translation.json +++ b/web/packages/base/locales/uk-UA/translation.json @@ -32,6 +32,7 @@ "set_password": "Встановити пароль", "sign_in": "Увійти", "incorrect_password": "Невірний пароль", + "incorrect_password_or_no_account": "", "pick_password_hint": "Введіть пароль, який ми можемо використовувати для шифрування ваших даних", "pick_password_caution": "Ми не зберігаємо ваш пароль, тому якщо ви забудете його, ми не зможемо допомогти вам відновити ваші дані без ключа відновлення.", "key_generation_in_progress": "Створюємо ключі шифрування...", @@ -39,11 +40,12 @@ "referral_source_hint": "Як ви дізналися про Ente? (необов'язково)", "referral_source_info": "Ми не відстежуємо встановлення застосунку. Але, якщо ви скажете нам, де ви нас знайшли, це допоможе!", "password_mismatch_error": "Паролі не співпадають", + "show_or_hide_password": "", "welcome_to_ente_title": "Вітаємо в ", "welcome_to_ente_subtitle": "Наскрізне зашифроване зберігання та спільний доступ до фотографій", "new_album": "Новий альбом", "create_albums": "Створити альбоми", - "enter_album_name": "Назва альбому", + "album_name": "Назва альбому", "close": "Закрити", "yes": "Так", "no": "Ні", @@ -529,9 +531,9 @@ "ml_consent_description": "

Якщо ви увімкнете машинне навчання, Ente буде витягувати інформацію, наприклад, геометрію обличчя, з файлів, у тому числі з тих, до яких ви надали доступ.

Це буде відбуватися на вашому пристрої, і будь-яка згенерована біометрична інформація буде наскрізно зашифрована.

Клацніть тут, щоби дізнатися більше про цю функцію в нашій політиці приватності

", "ml_consent_confirmation": "Я розумію, та бажаю увімкнути машинне навчання", "labs": "Лабораторії", - "passphrase_strength_weak": "Надійність пароля: Слабкий", - "passphrase_strength_moderate": "Надійність пароля: Помірний", - "passphrase_strength_strong": "Надійність пароля: Надійний", + "password_strength_weak": "Надійність пароля: Слабкий", + "password_strength_moderate": "Надійність пароля: Помірний", + "password_strength_strong": "Надійність пароля: Надійний", "preferences": "Налаштування", "language": "Мова", "advanced": "Додатково", @@ -624,6 +626,7 @@ "faster_upload_description": "Завантаження маршрутів через найближчі сервери", "open_ente_on_startup": "", "cast_album_to_tv": "Відтворення альбому на ТБ", + "cast_to_tv": "", "enter_cast_pin_code": "Введіть код, який ви бачите на телевізорі нижче, щоб з'єднатися з цим пристроєм.", "code": "Код", "pair_device_to_tv": "З'єднання між пристроями", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/vi-VN/translation.json b/web/packages/base/locales/vi-VN/translation.json index 04d0f30410..1dd20eea7c 100644 --- a/web/packages/base/locales/vi-VN/translation.json +++ b/web/packages/base/locales/vi-VN/translation.json @@ -32,6 +32,7 @@ "set_password": "Đặt mật khẩu", "sign_in": "Đăng nhập", "incorrect_password": "Mật khẩu không chính xác", + "incorrect_password_or_no_account": "", "pick_password_hint": "Vui lòng nhập mật khẩu mà chúng tôi có thể sử dụng để mã hóa dữ liệu của bạn", "pick_password_caution": "Chúng tôi không lưu trữ mật khẩu của bạn, vì vậy nếu bạn quên, chúng tôi sẽ không thể giúp bạn khôi phục dữ liệu mà không có khóa khôi phục.", "key_generation_in_progress": "Đang tạo khóa mã hóa...", @@ -39,11 +40,12 @@ "referral_source_hint": "Bạn đã nghe về Ente từ đâu? (tùy chọn)", "referral_source_info": "Chúng tôi không theo dõi cài đặt ứng dụng, sẽ rất hữu ích nếu bạn cho chúng tôi biết bạn đã tìm thấy chúng tôi ở đâu!", "password_mismatch_error": "Mật khẩu không khớp", + "show_or_hide_password": "", "welcome_to_ente_title": "Chào mừng đến với ", "welcome_to_ente_subtitle": "Lưu trữ và chia sẻ ảnh được mã hóa đầu cuối", "new_album": "Album mới", "create_albums": "Tạo album", - "enter_album_name": "Tên album", + "album_name": "Tên album", "close": "Đóng", "yes": "Có", "no": "Không", @@ -529,9 +531,9 @@ "ml_consent_description": "

Nếu bạn bật học máy, Ente sẽ trích xuất thông tin như hình dạng khuôn mặt từ các tệp, bao gồm cả những tệp được chia sẻ với bạn.

Điều này sẽ xảy ra trên thiết bị của bạn, và bất kỳ thông tin sinh trắc học nào được tạo ra sẽ được mã hóa đầu cuối.

Vui lòng nhấp vào đây để biết thêm chi tiết về tính năng này trong chính sách quyền riêng tư của chúng tôi

", "ml_consent_confirmation": "Tôi hiểu và muốn bật học máy", "labs": "Phòng thí nghiệm", - "passphrase_strength_weak": "Độ mạnh mật khẩu: Yếu", - "passphrase_strength_moderate": "Độ mạnh mật khẩu: Trung bình", - "passphrase_strength_strong": "Độ mạnh mật khẩu: Mạnh", + "password_strength_weak": "Độ mạnh mật khẩu: Yếu", + "password_strength_moderate": "Độ mạnh mật khẩu: Trung bình", + "password_strength_strong": "Độ mạnh mật khẩu: Mạnh", "preferences": "Tùy chọn", "language": "Ngôn ngữ", "advanced": "Nâng cao", @@ -624,6 +626,7 @@ "faster_upload_description": "Định tuyến tải lên qua các máy chủ gần đó", "open_ente_on_startup": "", "cast_album_to_tv": "Phát album trên TV", + "cast_to_tv": "", "enter_cast_pin_code": "Nhập mã bạn thấy trên TV bên dưới để ghép nối thiết bị này.", "code": "Mã", "pair_device_to_tv": "Ghép nối thiết bị", @@ -674,5 +677,11 @@ "theme": "", "system": "", "light": "", - "dark": "" + "dark": "", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/zh-CN/translation.json b/web/packages/base/locales/zh-CN/translation.json index 1f8a88523a..17431822a7 100644 --- a/web/packages/base/locales/zh-CN/translation.json +++ b/web/packages/base/locales/zh-CN/translation.json @@ -32,6 +32,7 @@ "set_password": "设置密码", "sign_in": "登录", "incorrect_password": "密码错误", + "incorrect_password_or_no_account": "", "pick_password_hint": "请输入我们可以用来加密您数据的密码", "pick_password_caution": "我们不会存储您的密码,因此如果您忘记密码, 我们将无法帮助您在没有恢复密钥的情况下恢复您的数据。", "key_generation_in_progress": "正在生成加密密钥...", @@ -39,11 +40,12 @@ "referral_source_hint": "您是如何知道Ente的? (可选的)", "referral_source_info": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "password_mismatch_error": "两次输入的密码不一致", + "show_or_hide_password": "", "welcome_to_ente_title": "欢迎来到 ", "welcome_to_ente_subtitle": "端到端加密的照片存储和共享", "new_album": "新建相册", "create_albums": "创建相册", - "enter_album_name": "相册名称", + "album_name": "相册名称", "close": "关闭", "yes": "是", "no": "否", @@ -529,9 +531,9 @@ "ml_consent_description": "

如果您启用机器学习,Ente 将从文件(包括与您共享的文件)中提取面部几何形状等信息。

这将在您的设备上发生,并且任何生成的生物特征信息都会被端到端加密。

请点击此处查看我们的隐私政策中有关此功能的更多详细信息

", "ml_consent_confirmation": "我了解了,并希望启用机器学习", "labs": "实验室", - "passphrase_strength_weak": "密码强度:较弱", - "passphrase_strength_moderate": "密码强度:中度", - "passphrase_strength_strong": "密码强度:强", + "password_strength_weak": "密码强度:较弱", + "password_strength_moderate": "密码强度:中度", + "password_strength_strong": "密码强度:强", "preferences": "首选项", "language": "语言", "advanced": "高级设置", @@ -624,6 +626,7 @@ "faster_upload_description": "通过附近的服务器路由上传", "open_ente_on_startup": "启动时打开 Ente", "cast_album_to_tv": "在电视上播放相册", + "cast_to_tv": "", "enter_cast_pin_code": "输入您在下面的电视上看到的代码来配对此设备。", "code": "代码", "pair_device_to_tv": "配对设备", @@ -674,5 +677,11 @@ "theme": "主题", "system": "系统", "light": "浅色", - "dark": "深色" + "dark": "深色", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/locales/zh-HK/translation.json b/web/packages/base/locales/zh-HK/translation.json index 0785857bf6..931b671f7a 100644 --- a/web/packages/base/locales/zh-HK/translation.json +++ b/web/packages/base/locales/zh-HK/translation.json @@ -10,7 +10,7 @@ "new_to_ente": "", "existing_user": "現有用戶", "enter_email": "輸入電郵地址", - "invalid_email_error": "輸入正確的電郵地址", + "invalid_email_error": "輸入有效的電郵地址", "required": "必填", "email_not_registered": "電郵未註冊", "email_already_registered": "電郵已被註冊", @@ -32,6 +32,7 @@ "set_password": "設定密碼", "sign_in": "登入", "incorrect_password": "密碼錯誤", + "incorrect_password_or_no_account": "", "pick_password_hint": "", "pick_password_caution": "", "key_generation_in_progress": "", @@ -39,11 +40,12 @@ "referral_source_hint": "", "referral_source_info": "", "password_mismatch_error": "", + "show_or_hide_password": "顯示或隱藏密碼", "welcome_to_ente_title": "歡迎來到 ", "welcome_to_ente_subtitle": "", "new_album": "新相簿", "create_albums": "創建相簿", - "enter_album_name": "相簿名稱", + "album_name": "相簿名稱", "close": "關閉", "yes": "是", "no": "否", @@ -56,12 +58,12 @@ "add_photos_count": "新增 {count, number} 個項目", "select_photos": "選擇照片", "file_upload": "上傳檔案", - "preparing": "", - "processed_counts": "", + "preparing": "準備中", + "processed_counts": "{{count, number}}/{{total, number}}", "upload_reading_metadata_files": "", "upload_cancelling": "取消剩餘上傳項目", - "upload_done": "", - "upload_skipped": "", + "upload_done": "{{count, number}} 個已上載", + "upload_skipped": "{{count, number}} 個已略過", "initial_load_delay_warning": "首次載入需時", "no_account": "未有帳戶", "existing_account": "已有帳戶", @@ -94,15 +96,15 @@ "exit_fullscreen": "退出全螢幕", "go_fullscreen": "進入全螢幕", "zoom": "縮放", - "play": "", - "pause": "", + "play": "播放", + "pause": "暫停", "previous": "", "next": "", "video_seek": "", - "quality": "", - "auto": "", - "original": "", - "speed": "", + "quality": "品質", + "auto": "自動", + "original": "原始", + "speed": "速度", "title_photos": "", "title_auth": "", "title_accounts": "", @@ -118,7 +120,7 @@ "selected_and_yours_count": "", "delete": "刪除", "favorite": "保存至最愛", - "convert": "", + "convert": "轉換", "multi_folder_upload": "", "upload_to_choice": "", "upload_to_single_album": "", @@ -168,8 +170,8 @@ "subscription": "訂閱", "manage_payment_method": "", "manage_family": "", - "family_plan": "", - "leave_family_plan": "", + "family_plan": "家庭計劃", + "leave_family_plan": "離開家庭計劃", "leave": "離開", "leave_family_plan_confirm": "", "choose_plan": "", @@ -235,8 +237,8 @@ "album": "相簿", "date": "日期", "description": "", - "file_type": "", - "magic": "", + "file_type": "檔案類型", + "magic": "魔術", "photos_count_zero": "", "photos_count_one": "", "photos_count": "", @@ -529,9 +531,9 @@ "ml_consent_description": "", "ml_consent_confirmation": "", "labs": "實驗室", - "passphrase_strength_weak": "密碼強度:弱", - "passphrase_strength_moderate": "密碼強度:中", - "passphrase_strength_strong": "密碼強度:強", + "password_strength_weak": "密碼強度:弱", + "password_strength_moderate": "密碼強度:中", + "password_strength_strong": "密碼強度:強", "preferences": "設定", "language": "語言", "advanced": "進階", @@ -624,6 +626,7 @@ "faster_upload_description": "", "open_ente_on_startup": "", "cast_album_to_tv": "", + "cast_to_tv": "", "enter_cast_pin_code": "", "code": "", "pair_device_to_tv": "配對設備", @@ -674,5 +677,11 @@ "theme": "主題", "system": "系統", "light": "亮色", - "dark": "暗色" + "dark": "暗色", + "streamable_videos": "", + "processing_videos_status": "", + "share_favorites": "", + "person_favorites": "", + "shared_favorites": "", + "added_by_name": "" } diff --git a/web/packages/base/next.config.base.js b/web/packages/base/next.config.base.js index 5bb855f792..9a69464580 100644 --- a/web/packages/base/next.config.base.js +++ b/web/packages/base/next.config.base.js @@ -64,7 +64,7 @@ const isDesktop = process.env._ENTE_IS_DESKTOP ? "1" : ""; /** * When we're running within the desktop app, also extract the version of the - * desktop app for use in our "X-Client-Package" string. + * desktop app for use in our "X-Client-Version" string. * * > The web app has continuous deployments, and doesn't have versions. */ @@ -116,9 +116,8 @@ const nextConfig = { // Ask Next to use a separate dist directory for the desktop during // development. This allows us run dev servers simultaneously for both web // and desktop code without them stepping on each others toes. - ...(process.env.NODE_ENV != "production" && isDesktop - ? { distDir: ".next-desktop" } - : {}), + ...(process.env.NODE_ENV != "production" && + isDesktop && { distDir: ".next-desktop" }), // Customize the webpack configuration used by Next.js. webpack: (config, { isServer }) => { diff --git a/web/packages/base/package.json b/web/packages/base/package.json index 9278992a43..c0c41ad7e6 100644 --- a/web/packages/base/package.json +++ b/web/packages/base/package.json @@ -5,29 +5,28 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@fontsource-variable/inter": "^5.2.5", - "@mui/icons-material": "^6.4.11", - "@mui/material": "^6.4.11", + "@fontsource-variable/inter": "^5.2.6", + "@mui/icons-material": "^7.1.1", + "@mui/material": "^7.1.1", "comlink": "^4.4.2", "ente-utils": "*", "formik": "^2.4.6", - "get-user-locale": "^2.3.2", - "i18next": "^24.2.3", + "get-user-locale": "^3.0.0", + "i18next": "^25.2.1", "i18next-resources-to-backend": "^1.2.1", - "idb": "^8.0.2", + "idb": "^8.0.3", "libsodium-wrappers-sumo": "^0.7.15", "nanoid": "^5.1.5", - "next": "^15.3.1", + "next": "^15.3.3", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-i18next": "^15.4.1", - "yup": "^1.6.1", - "zod": "^3.24.3" + "react-i18next": "^15.5.2", + "zod": "^3.25.61" }, "devDependencies": { "@types/libsodium-wrappers-sumo": "^0.7.8", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/react": "^19.1.7", + "@types/react-dom": "^19.1.6", "ente-build-config": "*" } } diff --git a/web/packages/base/session.ts b/web/packages/base/session.ts index b416b8ed9a..bf0aa9913c 100644 --- a/web/packages/base/session.ts +++ b/web/packages/base/session.ts @@ -1,57 +1,218 @@ -import { z } from "zod"; -import { decryptBox } from "./crypto"; -import { toB64 } from "./crypto/libsodium"; +import { getToken } from "ente-shared/storage/localStorage/helpers"; +import { z } from "zod/v4"; +import { decryptBox, encryptBox, generateKey } from "./crypto"; +import { isDevBuild } from "./env"; +import log from "./log"; +import { getAuthToken } from "./token"; /** - * Return the user's decrypted master key from session storage. + * Remove all data stored in session storage (data tied to the browser tab). * - * Precondition: The user should be logged in. + * See `docs/storage.md` for more details about session storage. Currently, only + * the following entries are stored in session storage: + * + * - "encryptionKey" + * - "keyEncryptionKey" (transient) */ -export const masterKeyFromSession = async () => { - const key = await masterKeyFromSessionIfLoggedIn(); - if (key) { - return key; - } else { - throw new Error( - "The user's master key was not found in session storage. Likely they are not logged in.", - ); - } +export const clearSessionStorage = () => sessionStorage.clear(); + +/** + * Schema of JSON string value for the "encryptionKey" and "keyEncryptionKey" + * keys strings stored in session storage. + */ +const SessionKeyData = z.object({ + encryptedData: z.string(), + key: z.string(), + nonce: z.string(), +}); + +type SessionKeyData = z.infer; + +const sessionKeyData = async (keyData: string): Promise => { + const key = await generateKey(); + const box = await encryptBox(keyData, key); + return { key, ...box }; +}; + +/** + * Return the user's decrypted master key (as a base64 string) from session + * storage, or throw if the session storage does not have the master key (which + * likely indicates that the user is not logged in). + */ +export const ensureMasterKeyFromSession = async () => { + const key = await masterKeyFromSession(); + if (!key) throw new Error("Master key not found in session"); + return key; }; /** * Return `true` if the user's encrypted master key is present in the session. * - * Use {@link masterKeyFromSessionIfLoggedIn} to get the actual master key after - * decrypting it. This function is instead useful as a quick check to verify if - * we have credentials at hand or not. + * Use {@link masterKeyFromSession} to get the actual master key after + * decrypting it. This function is a similar but quicker check to verify if we + * have credentials at hand or not, however it doesn't attempt to verify that + * the key present in the session can actually be decrypted. */ export const haveCredentialsInSession = () => !!sessionStorage.getItem("encryptionKey"); /** - * Return the decrypted user's master key from session storage if they are - * logged in, otherwise return `undefined`. + * Return the decrypted user's master key (as a base64 string) from session + * storage if they are logged in, otherwise return `undefined`. + * + * See also {@link ensureMasterKeyFromSession}, which is usually what we need. */ -export const masterKeyFromSessionIfLoggedIn = async () => { - // TODO: Same value as the deprecated getKey("encryptionKey") +export const masterKeyFromSession = async () => { const value = sessionStorage.getItem("encryptionKey"); if (!value) return undefined; - const { encryptedData, key, nonce } = EncryptionKeyAttributes.parse( + const { encryptedData, key, nonce } = SessionKeyData.parse( JSON.parse(value), ); return decryptBox({ encryptedData, nonce }, key); }; /** - * Variant of {@link masterKeyFromSession} that returns the master key as a - * base64 string. + * Save the user's encrypted master key in the session storage. If we're running + * in the context of our desktop app, also save it in the OS safe storage. + * + * See: [Note: Safe storage and interactive KEK attributes] + * + * @param masterKey The user's master key (as a base64 encoded string). */ -export const masterKeyB64FromSession = () => masterKeyFromSession().then(toB64); +export const saveMasterKeyInSessionAndSafeStore = async (masterKey: string) => { + await saveKeyInSessionStore("encryptionKey", masterKey); + try { + await globalThis.electron?.saveMasterKeyInSafeStorage(masterKey); + } catch (e) { + // [Note: Safe storage is best effort] + // + // The user might be running on an OS which does not provide secure + // storage. Practically this is rare, but it can happen, especially on + // Linux if the app is run in a desktop environment without libsecret. + // + // So intercept failures to read and write to safe storage, and + // gracefully degrade to how the web app behaves (ask the user for their + // password in each session). + log.warn("Failed to save master key in safe storage", e); + } +}; -// TODO: Same as B64EncryptionResult. Revisit. -const EncryptionKeyAttributes = z.object({ - encryptedData: z.string(), - key: z.string(), - nonce: z.string(), -}); +/** + * Save the provided key in session storage. + * + * @param keyName The name of the key use for the session storage entry. + * + * @param keyData The base64 encoded bytes of the key. + */ +const saveKeyInSessionStore = async (keyName: string, keyData: string) => { + sessionStorage.setItem( + keyName, + JSON.stringify(await sessionKeyData(keyData)), + ); +}; + +/** + * If we're running in the context of the desktop app, then read the master key + * from the OS safe storage and put it into the session storage (if it is not + * already present there). + * + * [Note: Safe storage and interactive KEK attributes] + * + * In the electron app we have the option of using the OS's safe storage (if + * available) to store the master key so that the user does not have to reenter + * their password each time they open the app. + * + * Such an ability is not present on browsers currently, so we need to ask the + * user for their password to derive the KEK for decrypting their master key + * each time they open the app in a new time (See: [Note: Key encryption key]). + * + * However, the default KEK parameters are not suitable for such frequent + * interactive usage. So for the user's convenience, we also derive an new (so + * called "intermediate") KEK using parameters suitable for interactive usage. + * This KEK is not saved to remote, it is only maintained locally. + * + * In either case, eventually we want the encrypted key to be available in the + * session for decrypting the user's files etc. In the web case, the page where + * the user reenters their password will put it there, while on desktop + * (assuming the key has already been saved to the OS safe storage), this + * {@link updateSessionFromElectronSafeStorageIfNeeded} function will do it. + */ +export const updateSessionFromElectronSafeStorageIfNeeded = async () => { + const electron = globalThis.electron; + if (!electron) return; + + if (haveCredentialsInSession()) return; + + let masterKey: string | undefined; + try { + masterKey = await electron.masterKeyFromSafeStorage(); + } catch (e) { + // See: [Note: Safe storage is best effort] + log.warn("Failed to read master key from safe storage", e); + } + if (masterKey) { + await saveKeyInSessionStore("encryptionKey", masterKey); + } +}; + +/** + * Return true if we both have the user's master key in session storage, and + * their auth token in KV DB. + */ +export const haveAuthenticatedSession = async () => { + if (!(await masterKeyFromSession())) return false; + const lsToken = getToken(); + const kvToken = await getAuthToken(); + // TODO: To avoid changing old behaviour, this currently relies on the token + // from local storage. Both should be the same though, so it throws an error + // on dev build (tag: Migration). + if (isDevBuild) { + if (lsToken != kvToken) + throw new Error("Local storage and indexed DB mismatch"); + } + return !!lsToken; +}; + +/** + * Save the user's encypted key encryption key ("key") in session store + * temporarily, until we get back here after completing the second factor. + * + * See: [Note: Stashing KEK in session store] + * + * @param kek The user's key encryption key (as a base64 string). + */ +export const stashKeyEncryptionKeyInSessionStore = (kek: string) => + saveKeyInSessionStore("keyEncryptionKey", kek); + +/** + * Return the decrypted user's key encryption key ("KEK") from session storage + * if present, otherwise return `undefined`. + * + * The key (if it was present) is also removed from session storage. + * + * @returns the previously stashed key (if any) as a base64 string. + * + * [Note: Stashing KEK in session store] + * + * During login, if the user has set a second factor (passkey or TOTP), then we + * need to redirect them to the accounts app or TOTP page to verify the second + * factor. This second factor verification happens after password verification, + * but simply storing the decrypted KEK in-memory wouldn't work because the + * second factor redirect can happen to a separate accounts app altogether. + * + * So instead, we stash the encrypted KEK in session store (using + * {@link stashKeyEncryptionKeyInSessionStore}), and after redirect, retrieve it + * (after clearing it) using {@link unstashKeyEncryptionKeyFromSession}. + */ +export const unstashKeyEncryptionKeyFromSession = async () => { + const value = sessionStorage.getItem("keyEncryptionKey"); + if (!value) return undefined; + + sessionStorage.removeItem("keyEncryptionKey"); + + const { encryptedData, key, nonce } = SessionKeyData.parse( + JSON.parse(value), + ); + return decryptBox({ encryptedData, nonce }, key); +}; diff --git a/web/packages/base/token.ts b/web/packages/base/token.ts new file mode 100644 index 0000000000..f23d7866c7 --- /dev/null +++ b/web/packages/base/token.ts @@ -0,0 +1,25 @@ +import { getKVS } from "./kv"; + +/** + * Return the user's auth token, if present. + * + * The user's auth token is stored in KV DB after they have successfully logged + * in. This function returns that saved auth token. + * + * The underlying data is stored in IndexedDB, and can be accessed from web + * workers. + */ +export const getAuthToken = () => getKVS("token"); + +/** + * Return the user's auth token, or throw an error. + * + * The user's auth token can be retrieved using {@link getAuthToken}. This + * function is a wrapper which throws an error if the token is not found (which + * should only happen if the user is not logged in). + */ +export const ensureAuthToken = async () => { + const token = await getAuthToken(); + if (!token) throw new Error("Not logged in"); + return token; +}; diff --git a/web/packages/base/types/ipc.ts b/web/packages/base/types/ipc.ts index 22cbf7ffdb..ed41ac0453 100644 --- a/web/packages/base/types/ipc.ts +++ b/web/packages/base/types/ipc.ts @@ -90,15 +90,15 @@ export interface Electron { * * If the key is not found, return `undefined`. * - * See also: {@link saveMasterKeyB64}. + * See also: {@link saveMasterKeyInSafeStorage}. */ - masterKeyB64: () => Promise; + masterKeyFromSafeStorage: () => Promise; /** - * Save the given {@link masterKeyB64} (encoded as a base64 string) to the + * Save the given {@link masterKey} (encoded as a base64 string) to the * persistent safe storage accessible to the desktop app. */ - saveMasterKeyB64: (masterKeyB64: string) => Promise; + saveMasterKeyInSafeStorage: (masterKey: string) => Promise; /** * Set or clear the callback {@link cb} to invoke whenever the app comes @@ -316,10 +316,10 @@ export interface Electron { * The behaviour is OS dependent. On macOS we use the `sips` utility, while * on Linux and Windows we use a `vips` bundled with our desktop app. * - * @param dataOrPathOrZipItem The file whose thumbnail we want to generate. - * It can be provided as raw image data (the contents of the image file), or - * the path to the image file, or a tuple containing the path of the zip - * file along with the name of an entry in it. + * @param pathOrZipItem The file whose thumbnail we want to generate. It can + * be provided as raw image data (the contents of the image file), or the + * path to the image file, or a tuple containing the path of the zip file + * along with the name of an entry in it. * * @param maxDimension The maximum width or height of the generated * thumbnail. @@ -329,14 +329,13 @@ export interface Electron { * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, maxDimension: number, maxSize: number, ) => Promise; /** - * Execute a FFmpeg {@link command} on the given - * {@link dataOrPathOrZipItem}. + * Execute a FFmpeg {@link command} on the given {@link pathOrZipItem}. * * This executes the command using a FFmpeg executable we bundle with our * desktop app. We also have a Wasm FFmpeg implementation that we use when @@ -349,11 +348,11 @@ export interface Electron { * (respectively {@link inputPathPlaceholder}, * {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}). * - * @param dataOrPathOrZipItem The bytes of the input file, or the path to - * the input file on the user's local disk, or the path to a zip file on the - * user's disk and the name of an entry in it. In all three cases, the data - * gets serialized to a temporary file, and then that path gets substituted - * in the FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. + * @param pathOrZipItem The path to the input file on the user's local disk, + * or the path to a zip file on the user's disk and the name of an entry in + * it. In the second case, the data gets serialized to a temporary file, and + * then that path (or if it was already a path) gets substituted in the + * FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. * * @param outputFileExtension The extension (without the dot, e.g. "jpeg") * to use for the output file that we ask FFmpeg to create in @@ -366,28 +365,52 @@ export interface Electron { */ ffmpegExec: ( command: FFmpegCommand, - dataOrPathOrZipItem: Uint8Array | string | ZipItem, + pathOrZipItem: string | ZipItem, outputFileExtension: string, ) => Promise; - // - ML + /** + * Determine the duration (in seconds) of the video present at + * {@link pathOrZipItem} using ffmpeg. + * + * This is a bespoke variant of {@link ffmpegExec} for the sole purpose of + * retrieving the video duration. + * + * @param pathOrZipItem The input file whose duration we want to determine. + * For more details, see the documentation of the {@link ffmpegExec} + * parameter with the same name. + * + * @returns The duration (in seconds) of the video referred to by + * {@link pathOrZipItem}. + */ + ffmpegDetermineVideoDuration: ( + pathOrZipItem: string | ZipItem, + ) => Promise; + + // - Utility process /** - * Create a new ML worker, terminating the older ones (if any). + * Trigger the creation of a new utility process of the given {@link type}, + * terminating the older ones (if any). * * This creates a new Node.js utility process, and sets things up so that we * can communicate directly with that utility process using a - * {@link MessagePort} that gets posted using "createMLWorker/port". + * {@link MessagePort} that gets posted on the "utilityProcessPort/" + * channel. * - * At the other end of that port will be an object that conforms to the - * {@link ElectronMLWorker} interface. + * The code running in the utility process is determined by the specific + * value of {@link type}. Thus, att the other end of that port will be an + * object that conforms to: + * + * - {@link ElectronMLWorker} interface, when type is "ml". * * For more details about the IPC flow, see: [Note: ML IPC]. * * Note: For simplicity of implementation, we assume that there is at most - * one outstanding call to {@link createMLWorker}. + * one outstanding call to {@link triggerCreateUtilityProcess} for a given + * {@link type}. */ - createMLWorker: () => void; + triggerCreateUtilityProcess: (type: UtilityProcessType) => void; // - Watch @@ -590,9 +613,12 @@ export interface Electron { clearPendingUploads: () => Promise; } +export type UtilityProcessType = "ml"; + /** - * The shape of the object exposed by the Node.js ML worker process on the - * message port that the web layer obtains by doing {@link createMLWorker}. + * The shape of the object exposed by the Node.js utility process listening on + * the other side message port that the web layer obtains by doing + * {@link triggerCreateUtilityProcess} with type "ml". */ export interface ElectronMLWorker { /** diff --git a/web/packages/base/utils/web.ts b/web/packages/base/utils/web.ts index f51583ae11..cb45280990 100644 --- a/web/packages/base/utils/web.ts +++ b/web/packages/base/utils/web.ts @@ -3,11 +3,12 @@ * folder by appending a temporary element to the DOM. * * @param url The URL that we want to download. See also - * {@link downloadAndRevokeObjectURL} and {@link downloadString}. + * {@link downloadAndRevokeObjectURL} and {@link downloadString}. The URL is + * revoked after initiating the download. * * @param fileName The name of downloaded file. */ -export const downloadURL = (url: string, fileName: string) => { +export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { const a = document.createElement("a"); a.style.display = "none"; a.href = url; @@ -18,15 +19,6 @@ export const downloadURL = (url: string, fileName: string) => { a.remove(); }; -/** - * A variant of {@link downloadURL} that also revokes the provided - * {@link objectURL} after initiating the download. - */ -export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { - downloadURL(url, fileName); - URL.revokeObjectURL(url); -}; - /** * Save the given string {@link s} as a file in the user's download folder. * diff --git a/web/packages/build-config/package.json b/web/packages/build-config/package.json index 4ab4368af4..c994d4fde2 100644 --- a/web/packages/build-config/package.json +++ b/web/packages/build-config/package.json @@ -4,15 +4,15 @@ "private": true, "type": "module", "devDependencies": { - "@eslint/js": "^9.25.1", - "eslint": "^9.25.1", + "@eslint/js": "^9.28.0", + "eslint": "^9.28.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "prettier": "^3.5.3", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-packagejson": "^2.5.10", + "prettier-plugin-packagejson": "^2.5.15", "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1" + "typescript-eslint": "^8.34.0" } } diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index fab7252efe..62e169aca4 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -37,9 +37,11 @@ import { import { LinkButtonUndecorated } from "ente-base/components/LinkButton"; import { type ButtonishProps } from "ente-base/components/mui"; import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator"; -import { SidebarDrawer } from "ente-base/components/mui/SidebarDrawer"; +import { + SidebarDrawer, + SidebarDrawerTitlebar, +} from "ente-base/components/mui/SidebarDrawer"; import { SingleInputForm } from "ente-base/components/SingleInputForm"; -import { Titlebar } from "ente-base/components/Titlebar"; import { EllipsizedTypography } from "ente-base/components/Typography"; import { useModalVisibility, @@ -256,9 +258,15 @@ export const FileInfo: React.FC = ({ onSelectPerson?.(personID); }; + const uploaderName = filePublicMagicMetadata(file)?.uploaderName; + return ( - +
= ({ }} /> )} + {uploaderName && ( + + {t("added_by_name", { name: uploaderName })} + + )} > = ({ {title} {caption} @@ -582,7 +596,9 @@ const Caption: React.FC = ({ const { values, errors, handleChange, handleSubmit, resetForm } = formik; if (!caption.length && !allowEdits) { - return <>; + // Visually take up some space, otherwise the info panel for the shared + // photos without a caption looks squished at the top. + return ; } return ( @@ -824,8 +840,8 @@ const RenameFileDialog: React.FC = ({ = ({ return ( - } diff --git a/web/packages/gallery/components/Upload.tsx b/web/packages/gallery/components/Upload.tsx new file mode 100644 index 0000000000..7f723875cf --- /dev/null +++ b/web/packages/gallery/components/Upload.tsx @@ -0,0 +1,7 @@ +/** + * The upload can be triggered by different buttons and flows in the UI, each of + * which is referred to as an "intent". + * + * The "intent" does not change the eventual upload outcome, only the UX flow. + */ +export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; diff --git a/web/packages/gallery/components/utils/use-file-input.ts b/web/packages/gallery/components/utils/use-file-input.ts index 44e923af22..7aec8d3d6e 100644 --- a/web/packages/gallery/components/utils/use-file-input.ts +++ b/web/packages/gallery/components/utils/use-file-input.ts @@ -115,7 +115,7 @@ export const useFileInput = ({ ...directoryOpts, ref: inputRef, onChange: handleChange, - ...(accept ? { accept } : {}), + ...(accept && { accept }), }), [directoryOpts, accept, handleChange], ); diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index e9d5b7bb28..076cee8b6c 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -17,6 +17,7 @@ import { Typography, type ModalProps, } from "@mui/material"; +import type { LocalUser } from "ente-accounts/services/user"; import { isDesktop } from "ente-base/app"; import { SpacedRow } from "ente-base/components/containers"; import { InlineErrorIndicator } from "ente-base/components/ErrorIndicator"; @@ -29,7 +30,6 @@ import { type ModalVisibilityProps } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import { lowercaseExtension } from "ente-base/file-name"; import { formattedListJoin, ut } from "ente-base/i18n"; -import type { LocalUser } from "ente-base/local-user"; import log from "ente-base/log"; import { FileInfo, @@ -781,30 +781,32 @@ export const FileViewer: React.FC = ({ // Modify the active annotated file if we found a file with // the same ID in the (possibly) updated files array. // - // Note the omission of the PhotoSwipe refresh: we don't - // refresh the PhotoSwipe dialog itself since that would - // cause the user to lose their pan / zoom etc. - // // This is not correct in its full generality, but it works - // fine in the specific cases we would need to handle: + // fine in the specific cases we would need to handle (and + // we want to avoid refreshing the entire UI unnecessarily + // lest the user lose their zoom/pan etc): // - // - In case of delete, we'll not get to this code branch. + // - In case of delete or toggling archived that caused the + // file is no longer part of the list that is shown, we'll + // not get to this code branch. // - // - In case of toggling archive, just updating the file - // attribute is enough, the UI state is derived from it; - // none of the other attributes of the annotated file - // currently depend on the archive status change. + // - In case of toggling archive otherwise, just updating + // the file attribute is enough, the UI state is derived + // from it; none of the other attributes of the annotated + // file currently depend on the archive status change. af = { ...af, file: updatedFile }; } else { - // Refreshing the current slide after the current file has - // gone will show the subsequent slide (since that would've - // now moved down to the current index). + // The file we were displaying is no longer part of the list + // of files that should be displayed. Refresh the slides, + // adjusting the indexes as necessary. // - // However, we might've been the last slide, in which case - // we need to go back one slide first. To determine this, - // also pass the expected count of files to our PhotoSwipe - // wrapper. - psRef.current?.refreshCurrentSlideContent(files.length); + // A special case is when we might've been the last slide, + // in which case we need to go back one slide first. To + // determine this, also pass the expected count of files to + // our PhotoSwipe wrapper. + psRef.current?.refreshCurrentSlideContentAfterRemove( + files.length, + ); } } else { // If there are no more files left, close the viewer. diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts index 6c5d4a3727..d0f0d9a172 100644 --- a/web/packages/gallery/components/viewer/data-source.ts +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -4,7 +4,7 @@ import { downloadManager } from "ente-gallery/services/download"; import { extractRawExif, parseExif } from "ente-gallery/services/exif"; import { hlsPlaylistDataForFile, - type HLSPlaylistData, + type HLSPlaylistDataForFile, } from "ente-gallery/services/video"; import type { EnteFile } from "ente-media/file"; import { fileCaption, filePublicMagicMetadata } from "ente-media/file-metadata"; @@ -452,10 +452,14 @@ const enqueueUpdates = async ( const updateVideo = ( videoURL: string | undefined, - hlsPlaylistData: HLSPlaylistData | undefined, + hlsPlaylistData: HLSPlaylistDataForFile, ) => { const videoURLD = videoURL ? { videoURL } : {}; - if (hlsPlaylistData) { + // See: [Note: Caching HLS playlist data] + // + // In brief, there are three cases: + if (typeof hlsPlaylistData == "object") { + // 1. If we have a playlist, we can cache it const { playlistURL: videoPlaylistURL, width, @@ -466,18 +470,9 @@ const enqueueUpdates = async ( createHLSPlaylistItemDataValidity(), ); } else { - // See: [Note: Caching HLS playlist data] - // - // TODO(HLS): As an optimization, we can handle the logged in vs - // public albums case separately once we have the status-diff state, - // we don't need to mark status-diff case as transient. - // - // Note that setting the transient flag is not too expensive, since - // the underlying videoURL is still cached by the download manager. - // So effectively, under normal circumstance, it just adds one API - // call (to recheck if an HLS playlist now exists for the given - // file). - update({ ...videoURLD, isTransient: true }); + // 2. if the file is not eligible ("skip"), we can cache it. + // 3. Otherwise it's transient and shouldn't be cached indefinitely. + update({ ...videoURLD, isTransient: hlsPlaylistData != "skip" }); } }; @@ -526,7 +521,7 @@ const enqueueUpdates = async ( } try { - let hlsPlaylistData: HLSPlaylistData | undefined; + let hlsPlaylistData: HLSPlaylistDataForFile; if (file.metadata.fileType == FileType.video) { hlsPlaylistData = await hlsPlaylistDataForFile( file, @@ -534,7 +529,10 @@ const enqueueUpdates = async ( ); // We have a HLS playlist, and the user didn't request the original. // Early return so that we don't initiate a fetch for the original. - if (hlsPlaylistData && opts?.videoQuality != "original") { + if ( + typeof hlsPlaylistData == "object" && + opts?.videoQuality != "original" + ) { updateVideo(undefined, hlsPlaylistData); return; } @@ -656,10 +654,10 @@ const thumbnailDimensions = ( return { width: thumbnailWidth, height: thumbnailHeight }; }; /** - * Return a new validity for a HLS playlist containing presigned URLs. + * Return a new validity for a HLS playlist containing pre-signed URLs. * * The content chunks in HLS playlist generated by - * {@link hlsPlaylistDataForFile} use presigned URLs generated by remote (see + * {@link hlsPlaylistDataForFile} use pre-signed URLs generated by remote (see * `PreSignedRequestValidityDuration` in the museum source). These have a * validity of 7 days. We keep a 2 day buffer, and consider any item data that * uses such playlist as stale after 5 days. diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 69468e8484..09ec549ce7 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -315,9 +315,13 @@ export class FileViewerPhotoSwipe { const videoQuality = intendedVideoQualityForFileID(file.id); - const itemData = itemDataForFile(file, { videoQuality }, () => - pswp.refreshSlideContent(index), - ); + const itemData = itemDataForFile(file, { videoQuality }, () => { + // When we get updated item data, + // 1. Clear cached data. + _currentAnnotatedFile = undefined; + // 2. Request a refresh. + pswp.refreshSlideContent(index); + }); if (itemData.fileType === FileType.video) { const { videoPlaylistURL, videoURL } = itemData; @@ -350,9 +354,32 @@ export class FileViewerPhotoSwipe { }); /** - * Last state of the live photo playback toggle. + * File IDs for which we've already done initial live photo playback + * attempts in the current session (invocation of file viewer). + * + * This not only allows us to skip triggering initial playback when + * coming back to the same slide again (a behavioural choice), it also + * allows us to skip retriggering playback when decoding of the image + * component completes (a functional need). */ - let livePhotoPlay = true; + const livePhotoInitialVisitedFileIDs = new Set(); + + /** + * Last state of the live photo playback on initial display. + */ + let livePhotoPlayInitial = true; + + /** + * Set to the event listener that will be called at the end of the + * initial playback of a live photo on the currently displayed slide. + * + * This will be present only during the initial playback (it will be + * cleared when initial playback completes), and can also thus be used + * as a pseudo `isPlayingLivePhotoInitial`. + */ + let livePhotoPlayInitialEndedEvent: + | { listener: () => void; video: HTMLVideoElement } + | undefined; /** * Last state of the live photo muted toggle. @@ -371,23 +398,129 @@ export class FileViewerPhotoSwipe { /** * Update the state of the given {@link videoElement} and the - * {@link livePhotoPlayButtonElement} to reflect {@link livePhotoPlay}. + * {@link livePhotoPlayButtonElement} to reflect + * {@link livePhotoPlayInitial}. + * + * [Note: Live photo playback] + * + * 1. When opening a live photo, play it once unless + * {@link livePhotoPlayInitial} is disabled. This is the behaviour + * controlled by the {@link livePhotoUpdatePlayInitial} function. + * + * 2. If the user toggles playback of during the initial video playback, + * then remember their choice for the current session (invocation of + * file viewer) by disabling {@link livePhotoPlayInitial}. + * + * 3. Also keep track of which files have already been initial played in + * the current session (using {@link livePhotoInitialVisitedFileIDs}). + * + * 3. Post initial playback, user can play the video again in a loop by + * activating the {@link livePhotoPlayButtonElement}, which triggers + * the {@link livePhotoUpdatePlayToggle} function (and also resets + * {@link livePhotoPlayInitial}). */ - const livePhotoUpdatePlay = (video: HTMLVideoElement) => { + const livePhotoUpdatePlayInitial = (video: HTMLVideoElement) => { + livePhotoUpdateUIState(video); + + // Ignore if we've already visited this file. + const currFileID = currSlideData().fileID; + if (livePhotoInitialVisitedFileIDs.has(currFileID)) { + return; + } + + // Otherwise mark it as visited. + livePhotoInitialVisitedFileIDs.add(currFileID); + + // Remove any loop attributes we might've inherited from a + // previously displayed live photos elements on the current slide. + video.removeAttribute("loop"); + + // Clear any other initial playback listeners. + if (livePhotoPlayInitialEndedEvent) { + const { video, listener } = livePhotoPlayInitialEndedEvent; + video.removeEventListener("ended", listener); + livePhotoPlayInitialEndedEvent = undefined; + } + + if (livePhotoPlayInitial) { + // Initial playback is enabled - Play the video once. + // + // There are a few playback cases (initial, resumed, adjacent + // slide with a reused video element, new slide with a new video + // element). Always start at the beginning in all cases for user + // the feel the app is responding consistently. + video.currentTime = 0; + void abortablePlayVideo(video); + video.style.display = "initial"; + const listener = () => { + livePhotoPlayInitialEndedEvent = undefined; + livePhotoUpdateUIState(video); + }; + livePhotoPlayInitialEndedEvent = { video, listener }; + video.addEventListener("ended", listener, { once: true }); + } else { + video.pause(); + } + + livePhotoUpdateUIState(video); + }; + + const livePhotoUpdateUIState = (video: HTMLVideoElement) => { const button = livePhotoPlayButtonElement; if (button) showIf(button, true); - if (livePhotoPlay) { - button?.classList.remove("pswp-ente-off"); - void abortablePlayVideo(video); - video.style.display = "initial"; - } else { + if (video.paused || video.ended) { button?.classList.add("pswp-ente-off"); - video.pause(); video.style.display = "none"; + } else { + button?.classList.remove("pswp-ente-off"); + video.style.display = "initial"; } }; + /** + * See: [Note: Live photo playback] + * + * This function handles the playback toggled via an explicit user + * action (button activation or keyboard shortcut). + */ + const livePhotoUpdatePlayToggle = (video: HTMLVideoElement) => { + if (video.paused || video.ended) { + // Add the loop attribute. + video.setAttribute("loop", ""); + + // Take an explicit playback trigger as a signal to reset the + // initial playback flag. + // + // This is the only way for the user to reset the initial + // playback state (short of repopening the file viewer). + livePhotoPlayInitial = true; + + video.currentTime = 0; + void abortablePlayVideo(video); + } else { + // Remove the loop attribute (not necessarily needed because we + // remove it on slide change too, but good to clean up after + // ourselves). + video.removeAttribute("loop"); + + // If we're in the middle of the initial playback, remember the + // user's choice to disable autoplay. + if (livePhotoPlayInitialEndedEvent) { + livePhotoPlayInitial = false; + + // And reset the event handler. + const { video, listener } = livePhotoPlayInitialEndedEvent; + video.removeEventListener("ended", listener); + livePhotoPlayInitialEndedEvent = undefined; + } + + video.pause(); + } + + livePhotoUpdateUIState(video); + }; + /** * A wrapper over video.play that prevents Chrome from spamming the * console with errors about interrupted plays when scrolling through @@ -439,8 +572,7 @@ export class FileViewerPhotoSwipe { const video = livePhotoVideoOnSlide(pswp.currSlide); if (!buttonElement || !video) return; - livePhotoPlay = !livePhotoPlay; - livePhotoUpdatePlay(video); + livePhotoUpdatePlayToggle(video); }; /** @@ -600,10 +732,12 @@ export class FileViewerPhotoSwipe { updateVideoControlsAndPlayback(currSlideData()); } - // Rest of this function deals with live photos. + if (fileType != FileType.livePhoto || !videoURL) { + // Not a live photo, or its video hasn't loaded yet. + return; + } - if (fileType != FileType.livePhoto) return; - if (!videoURL) return; + // Rest of this function deals with live photos. // This slide is displaying a live photo. Append a video element to // show its video part. @@ -630,7 +764,7 @@ export class FileViewerPhotoSwipe { // already been called, but now "contentAppend" is happening. if (currSlideData().fileID == fileID) { - livePhotoUpdatePlay(video); + livePhotoUpdatePlayInitial(video); livePhotoUpdateMute(video); } }); @@ -905,7 +1039,7 @@ export class FileViewerPhotoSwipe { pswp.on("change", () => { const video = livePhotoVideoOnSlide(pswp.currSlide); if (video) { - livePhotoUpdatePlay(video); + livePhotoUpdatePlayInitial(video); } else { // Not a live photo, or its video hasn't loaded yet. showIf(buttonElement, false); @@ -1110,7 +1244,13 @@ export class FileViewerPhotoSwipe { const handleSeekBackOrPreviousSlide = () => { const video = videoVideoEl; - if (video && !isUserLikelyNavigatingBetweenSlides()) { + if ( + video && + !isUserLikelyNavigatingBetweenSlides() && + // If the video is at the beginning, then use the left arrow to + // move to the preview slide. + video.currentTime > 0 + ) { video.currentTime = Math.max(video.currentTime - 5, 0); } else { handlePreviousSlide(); @@ -1119,7 +1259,13 @@ export class FileViewerPhotoSwipe { const handleSeekForwardOrNextSlide = () => { const video = videoVideoEl; - if (video && !isUserLikelyNavigatingBetweenSlides()) { + if ( + video && + !isUserLikelyNavigatingBetweenSlides() && + // If the video has ended, then use right arrow to move to the + // next slide. + !video.ended + ) { video.currentTime = video.currentTime + 5; } else { handleNextSlide(); @@ -1386,18 +1532,43 @@ export class FileViewerPhotoSwipe { /** * Reload the current slide, asking the data source for its data afresh. + */ + refreshCurrentSlideContent() { + this.pswp.refreshSlideContent(this.pswp.currIndex); + } + + /** + * Reload the PhotoSwipe dialog (without recreating it) if the current slide + * that was being viewed is no longer part of the list of files that should + * be shown. This can happen when the user deleted the file, or if they + * marked it archived in a context (like "All") where archived files are not + * being shown. * * @param expectedFileCount The count of files that we expect to show after - * the refresh. If provided, this is used to (circle) go back to the first - * slide when the slide which we were at previously is not available anymore - * (e.g. when deleting the last file in a sequence). + * the refresh. */ - refreshCurrentSlideContent(expectedFileCount?: number) { - if (expectedFileCount && this.pswp.currIndex >= expectedFileCount) { - this.pswp.goTo(0); - } else { - this.pswp.refreshSlideContent(this.pswp.currIndex); + refreshCurrentSlideContentAfterRemove(newFileCount: number) { + // Refresh the slide, and its subsequent neighbour. + // + // To see why, consider item at index 3 was removed. After refreshing, + // the contents of the item previously at index 4, and now at index 3, + // would be displayed. But the preloaded slide next to us (showing item + // at index 4) would already be displaying the same item, so that also + // needs to be refreshed to displaying the item previously at index 5 + // (now at index 4). + const refreshSlideAndNextNeighbour = (i: number) => { + this.pswp.refreshSlideContent(i); + this.pswp.refreshSlideContent(i + 1 == newFileCount ? 0 : i + 1); + }; + + if (this.pswp.currIndex >= newFileCount) { + // If the last slide was removed, take one step back first (the code + // that calls us ensures that we don't get called if there are no + // more slides left). + this.pswp.prev(); } + + refreshSlideAndNextNeighbour(this.pswp.currIndex); } /** @@ -1500,7 +1671,7 @@ const hlsVideoControlsHTML = () => ` // playsinline will play the video inline on mobile browsers (where the default // is to open a full screen player). const livePhotoVideoHTML = (videoURL: string) => ` -