diff --git a/.github/assets/github-badge.png b/.github/assets/github-badge.png deleted file mode 100644 index ef3207c95..000000000 Binary files a/.github/assets/github-badge.png and /dev/null differ diff --git a/.github/workflows/auth-crowdin.yml b/.github/workflows/auth-crowdin.yml index f269a8c8d..811def939 100644 --- a/.github/workflows/auth-crowdin.yml +++ b/.github/workflows/auth-crowdin.yml @@ -2,12 +2,12 @@ name: "Sync Crowdin translations (auth)" on: push: + branches: [main] paths: # Run workflow when auth's intl_en.arb is changed - "mobile/lib/l10n/arb/app_en.arb" # Or the workflow itself is changed - ".github/workflows/auth-crowdin.yml" - branches: [main] schedule: # See: [Note: Run workflow on specific days of the week] - cron: "50 1 * * 2,5" @@ -28,7 +28,7 @@ jobs: base_path: "auth/" config: "auth/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-auth create_pull_request: true diff --git a/.github/workflows/auth-lint.yml b/.github/workflows/auth-lint.yml index 1b45a2d32..6504e0646 100644 --- a/.github/workflows/auth-lint.yml +++ b/.github/workflows/auth-lint.yml @@ -3,13 +3,13 @@ name: "Lint (auth)" on: # Run on every push to a branch other than main that changes auth/ push: - branches-ignore: [main] + branches-ignore: [main, "deploy/**"] paths: - "auth/**" - ".github/workflows/auth-lint.yml" env: - FLUTTER_VERSION: "3.16.9" + FLUTTER_VERSION: "3.19.3" jobs: lint: diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index bade58d25..da888901d 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -29,11 +29,11 @@ on: - "auth-v*" env: - FLUTTER_VERSION: "3.13.4" + FLUTTER_VERSION: "3.19.3" jobs: build-ubuntu: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 defaults: run: @@ -72,6 +72,8 @@ jobs: SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} - name: Build PlayStore AAB + # 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 env: @@ -83,7 +85,7 @@ jobs: - name: Install dependencies for desktop build run: | sudo apt-get update -y - sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate + sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi7 - name: Install appimagetool run: | @@ -92,8 +94,6 @@ jobs: mv appimagetool /usr/local/bin/ - name: Build desktop app - # Temporarily disable desktop builds - if: false run: | flutter config --enable-linux-desktop dart pub global activate flutter_distributor @@ -118,6 +118,8 @@ jobs: updateOnlyUnreleased: true - name: Upload AAB to PlayStore + # 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') uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} @@ -149,8 +151,6 @@ jobs: run: mkdir artifacts - name: Build Windows installer - # Temporarily disable desktop builds - if: false run: | flutter config --enable-windows-desktop dart pub global activate flutter_distributor @@ -159,13 +159,9 @@ jobs: mv dist/**/ente_auth-*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe - name: Retain Windows EXE and DLLs - # Temporarily disable desktop builds - if: false run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows - name: Code sign Windows installer and EXE - # Temporarily disable desktop builds - if: false uses: dlemstra/code-sign-action@v1 with: certificate: "${{ secrets.WINDOWS_CERTIFICATE }}" @@ -175,9 +171,10 @@ jobs: auth/ente-${{ github.ref_name }}-windows/auth.exe - name: Zip Windows EXE and DLLs - # Temporarily disable desktop builds - if: false - run: tar.exe -a -c -f auth/artifacts/ente-${{ github.ref_name }}-windows.zip auth/ente-${{ github.ref_name }}-windows + 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 - name: Create a draft GitHub release uses: ncipollo/release-action@v1 @@ -248,8 +245,6 @@ jobs: run: mkdir artifacts - name: Build macOS DMG - # Temporarily disable desktop builds - if: false run: | flutter config --enable-macos-desktop dart pub global activate flutter_distributor @@ -257,16 +252,12 @@ jobs: mv dist/**/ente_auth-*-macos.dmg artifacts/ente-${{ github.ref_name }}.dmg - name: Code sign DMG - # Temporarily disable desktop builds - if: false run: | CERT_NAME=$(security find-identity -v -p codesigning | grep "Developer ID Application" | awk -F'"' '{print $2}' | grep -m1 "") codesign --force --timestamp --sign "$CERT_NAME" --options runtime artifacts/ente-${{ github.ref_name }}.dmg codesign --verify --verbose=4 artifacts/ente-${{ github.ref_name }}.dmg - name: Notarize and staple DMG - # Temporarily disable desktop builds - if: false run: | xcrun notarytool submit artifacts/ente-${{ github.ref_name }}.dmg \ --wait \ @@ -279,6 +270,9 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + - name: Generate checksums + run: shasum -a 256 artifacts/ente-* > artifacts/sha256sum-macos + - name: Create a draft GitHub release uses: ncipollo/release-action@v1 with: diff --git a/.github/workflows/copycat-db-release.yaml b/.github/workflows/copycat-db-release.yaml new file mode 100644 index 000000000..5ec942879 --- /dev/null +++ b/.github/workflows/copycat-db-release.yaml @@ -0,0 +1,24 @@ +name: "Release (copycat-db)" + +on: + workflow_dispatch: # Run manually + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + name: Check out code + + - uses: mr-smithers-excellent/docker-build-push@v6 + name: Build & Push + with: + dockerfile: infra/copycat-db/Dockerfile + directory: infra/copycat-db + image: ente/copycat-db + registry: rg.fr-par.scw.cloud + enableBuildKit: true + buildArgs: GIT_COMMIT=${GITHUB_SHA} + tags: ${GITHUB_SHA}, latest + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docs-verify-build.yml b/.github/workflows/docs-verify-build.yml index addb52a05..a57f71c86 100644 --- a/.github/workflows/docs-verify-build.yml +++ b/.github/workflows/docs-verify-build.yml @@ -6,7 +6,7 @@ name: "Verify build (docs)" on: # Run on every push to a branch other than main that changes docs/ push: - branches-ignore: [main] + branches-ignore: [main, "deploy/**"] paths: - "docs/**" - ".github/workflows/docs-verify-build.yml" diff --git a/.github/workflows/mobile-crowdin.yml b/.github/workflows/mobile-crowdin.yml index 35b4c3876..5c52b59ad 100644 --- a/.github/workflows/mobile-crowdin.yml +++ b/.github/workflows/mobile-crowdin.yml @@ -2,12 +2,12 @@ name: "Sync Crowdin translations (mobile)" on: push: + branches: [main] paths: # Run workflow when mobiles's intl_en.arb is changed - "mobile/lib/l10n/intl_en.arb" # Or the workflow itself is changed - ".github/workflows/mobile-crowdin.yml" - branches: [main] schedule: # See: [Note: Run workflow on specific days of the week] - cron: "40 1 * * 2,5" @@ -28,7 +28,7 @@ jobs: base_path: "mobile/" config: "mobile/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-mobile create_pull_request: true diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index cbbbbcfbb..48e38dc6c 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -3,7 +3,7 @@ name: "Lint (mobile)" on: # Run on every push to a branch other than main that changes mobile/ push: - branches-ignore: [main, f-droid] + branches-ignore: [main, f-droid, "deploy/**"] paths: - "mobile/**" - ".github/workflows/mobile-lint.yml" diff --git a/.github/workflows/server-lint.yml b/.github/workflows/server-lint.yml index c051d0290..d25f2adcc 100644 --- a/.github/workflows/server-lint.yml +++ b/.github/workflows/server-lint.yml @@ -3,7 +3,7 @@ name: "Lint (server)" on: # Run on every push to a branch other than main that changes server/ push: - branches-ignore: [main] + branches-ignore: [main, "deploy/**"] paths: - "server/**" - ".github/workflows/server-lint.yml" diff --git a/.github/workflows/server-publish.yml b/.github/workflows/server-publish.yml new file mode 100644 index 000000000..393fa899d --- /dev/null +++ b/.github/workflows/server-publish.yml @@ -0,0 +1,38 @@ +name: "Publish (server)" + +on: + # Run manually, providing it the commit. + # + # To obtain the commit from the currently deployed museum, do: + # curl -s https://api.ente.io/ping | jq -r '.id' + # + # See server/docs/publish.md for more details. + workflow_dispatch: + inputs: + commit: + description: "Commit to publish the image from" + type: string + required: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} + + - name: Build and push + uses: mr-smithers-excellent/docker-build-push@v6 + with: + dockerfile: server/Dockerfile + directory: server + # Resultant package name will be ghcr.io/ente-io/server + image: server + registry: ghcr.io + enableBuildKit: true + buildArgs: GIT_COMMIT=${{ inputs.commit }} + tags: ${{ inputs.commit }}, latest + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/server-release.yml b/.github/workflows/server-release.yml index 8f0281951..fa0215530 100644 --- a/.github/workflows/server-release.yml +++ b/.github/workflows/server-release.yml @@ -7,11 +7,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - name: Check out code + - name: Checkout code + uses: actions/checkout@v4 - - uses: mr-smithers-excellent/docker-build-push@v6 - name: Build & Push + - name: Build and push + uses: mr-smithers-excellent/docker-build-push@v6 with: dockerfile: server/Dockerfile directory: server diff --git a/.github/workflows/web-crowdin.yml b/.github/workflows/web-crowdin.yml index 8733167d6..f834e62f3 100644 --- a/.github/workflows/web-crowdin.yml +++ b/.github/workflows/web-crowdin.yml @@ -2,12 +2,12 @@ name: "Sync Crowdin translations (web)" on: push: + branches: [main] paths: # Run workflow when web's en-US/translation.json is changed - "web/apps/photos/public/locales/en-US/translation.json" # Or the workflow itself is changed - ".github/workflows/web-crowdin.yml" - branches: [main] schedule: # [Note: Run workflow on specific days of the week] # @@ -34,7 +34,7 @@ jobs: base_path: "web/" config: "web/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-web create_pull_request: true diff --git a/.github/workflows/web-deploy-payments.yml b/.github/workflows/web-deploy-payments.yml new file mode 100644 index 000000000..c428d88bc --- /dev/null +++ b/.github/workflows/web-deploy-payments.yml @@ -0,0 +1,43 @@ +name: "Deploy (payments)" + +on: + push: + # Run workflow on pushes to the deploy/payments + branches: [deploy/payments] + +jobs: + deploy: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: web + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "docs/yarn.lock" + + - name: Install dependencies + run: yarn install + + - name: Build payments + run: yarn build:payments + + - name: Publish payments + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/payments + directory: web/apps/payments/out + wranglerVersion: "3" diff --git a/.github/workflows/web-lint.yml b/.github/workflows/web-lint.yml index 7f5d27002..0dc11aa0e 100644 --- a/.github/workflows/web-lint.yml +++ b/.github/workflows/web-lint.yml @@ -3,7 +3,7 @@ name: "Lint (web)" on: # Run on every push to a branch other than main that changes web/ push: - branches-ignore: [main] + branches-ignore: [main, "deploy/**"] paths: - "web/**" - ".github/workflows/web-lint.yml" diff --git a/.github/workflows/web-nightly.yml b/.github/workflows/web-nightly.yml index a800a4b73..89d5ecaa5 100644 --- a/.github/workflows/web-nightly.yml +++ b/.github/workflows/web-nightly.yml @@ -78,6 +78,19 @@ jobs: directory: web/apps/cast/out wranglerVersion: "3" + - name: Build payments + run: yarn build:payments + + - name: Publish payments + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-payments + directory: web/apps/payments/out + wranglerVersion: "3" + - name: Build photos run: yarn build:photos env: diff --git a/.github/workflows/web-preview.yml b/.github/workflows/web-preview.yml index 4e86d9a81..2ad73b7a1 100644 --- a/.github/workflows/web-preview.yml +++ b/.github/workflows/web-preview.yml @@ -12,6 +12,7 @@ on: - "accounts" - "auth" - "cast" + - "payments" - "photos" jobs: diff --git a/README.md b/README.md index 68632ea3b..88488d11c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ existing users will be grandfathered in. [](https://apps.apple.com/app/id6444121398) [](https://play.google.com/store/apps/details?id=io.ente.auth) [](https://f-droid.org/packages/io.ente.auth/) -[](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) +[](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) [](https://auth.ente.io) diff --git a/auth/.gitignore b/auth/.gitignore index 3d6f77b84..87abd6a1c 100644 --- a/auth/.gitignore +++ b/auth/.gitignore @@ -18,6 +18,11 @@ *.iws .idea/ +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ @@ -35,3 +40,4 @@ lib/generated_plugin_registrant.dart !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages android/key.properties +dist/ \ No newline at end of file diff --git a/auth/.metadata b/auth/.metadata index 42069fa1b..39ee784e4 100644 --- a/auth/.metadata +++ b/auth/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - channel: unknown + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "[user-branch]" project_type: app @@ -13,17 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: linux - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: macos - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: windows - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 # User provided section diff --git a/auth/README.md b/auth/README.md index 71ae1f93b..e2e03f023 100644 --- a/auth/README.md +++ b/auth/README.md @@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid. +### Desktop + +You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) +a native desktop app from this repository's GitHub releases. The desktop app +works on Windows, Linux and macOS. + ### Web You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding -or managing your secrets, please use our mobile app. - -### Desktop - -A native desktop app is coming soon! +or managing your secrets, please use our mobile or desktop app. ## 🧑‍💻 Build from source diff --git a/auth/android/app/build.gradle b/auth/android/app/build.gradle index 916e3b3c9..5621b08b6 100644 --- a/auth/android/app/build.gradle +++ b/auth/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -46,7 +46,7 @@ android { defaultConfig { applicationId "io.ente.auth" - minSdkVersion 20 + minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -56,11 +56,11 @@ android { signingConfigs { release { - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null - keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS") - keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD") - storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD") - } + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null + keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS") + keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD") + storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD") + } } flavorDimensions "default" @@ -109,6 +109,7 @@ dependencies { implementation 'io.sentry:sentry-android:2.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:multidex:1.0.3' + implementation 'com.google.guava:guava:28.2-android' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index b248ad8cb..3e8f8ce67 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -36,7 +36,9 @@ }, { "title": "BorgBase", - "altNames": ["borg"], + "altNames": [ + "borg" + ], "slug": "BorgBase" }, { @@ -46,11 +48,17 @@ { "title": "Bybit" }, + { + "title": "CERN" + }, { "title": "Channel Island Hosting", "slug": "cih", "hex": "D14633" }, + { + "title": "ConfigCat" + }, { "title": "Cloudflare" }, @@ -62,6 +70,13 @@ { "title": "Crowdpear" }, + { + "title": "DCS", + "altNames": [ + "Digital Combat Simulator" + ], + "slug": "dcs" + }, { "title": "DEGIRO" }, @@ -107,9 +122,14 @@ }, { "title": "Gosuslugi", - "altNames": ["Госуслуги"], + "altNames": [ + "Госуслуги" + ], "slug": "Gosuslugi" }, + { + "title": "Habbo" + }, { "title": "Healthchecks.io", "slug": "healthchecks" @@ -172,13 +192,24 @@ }, { "title": "Mastodon", - "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"], + "altNames": [ + "mstdn", + "fediscience", + "mathstodon", + "fosstodon" + ], "slug": "mastodon", "hex": "6364FF" }, + { + "title": "Mercado Livre", + "slug": "mercado_livre" + }, { "title": "Murena", - "altNames": ["eCloud"], + "altNames": [ + "eCloud" + ], "slug": "ecloud" }, { @@ -267,11 +298,18 @@ "title": "Revolt", "hex": "858585" }, + { + "title": "Rockstar Games", + "slug": "rockstar_games" + }, { "title": "Rust Language Forum", "slug": "rust_language_forum", "hex": "000000" }, + { + "title": "Sendgrid" + }, { "title": "service-bw" }, @@ -356,15 +394,24 @@ { "title": "Wise" }, + { + "title": "WYZE", + "slug": "wyze" + }, { "title": "X", - "altNames": ["twitter"], + "altNames": [ + "twitter" + ], "slug": "x" }, { "title": "Yandex", - "altNames": ["Ya", "Яндекс"], + "altNames": [ + "Ya", + "Яндекс" + ], "slug": "Yandex" } ] -} +} \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/CERN.svg b/auth/assets/custom-icons/icons/CERN.svg new file mode 100644 index 000000000..0e4b82e1f --- /dev/null +++ b/auth/assets/custom-icons/icons/CERN.svg @@ -0,0 +1,47 @@ + + + + + + diff --git a/auth/assets/custom-icons/icons/configcat.svg b/auth/assets/custom-icons/icons/configcat.svg new file mode 100644 index 000000000..cfecd22b0 --- /dev/null +++ b/auth/assets/custom-icons/icons/configcat.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/dcs.svg b/auth/assets/custom-icons/icons/dcs.svg new file mode 100644 index 000000000..dd7c41ccb --- /dev/null +++ b/auth/assets/custom-icons/icons/dcs.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/habbo.svg b/auth/assets/custom-icons/icons/habbo.svg new file mode 100644 index 000000000..746bcdb22 --- /dev/null +++ b/auth/assets/custom-icons/icons/habbo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/mercado_livre.svg b/auth/assets/custom-icons/icons/mercado_livre.svg new file mode 100644 index 000000000..7f4db5fd5 --- /dev/null +++ b/auth/assets/custom-icons/icons/mercado_livre.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/rockstar_games.svg b/auth/assets/custom-icons/icons/rockstar_games.svg new file mode 100644 index 000000000..bf2061c2b --- /dev/null +++ b/auth/assets/custom-icons/icons/rockstar_games.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/sendgrid.svg b/auth/assets/custom-icons/icons/sendgrid.svg new file mode 100644 index 000000000..1562adab9 --- /dev/null +++ b/auth/assets/custom-icons/icons/sendgrid.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/wyze.svg b/auth/assets/custom-icons/icons/wyze.svg new file mode 100644 index 000000000..89d252c15 --- /dev/null +++ b/auth/assets/custom-icons/icons/wyze.svg @@ -0,0 +1 @@ +Wyze \ No newline at end of file diff --git a/auth/assets/icon-light-adaptive-fg.png b/auth/assets/generation-icons/icon-light-adaptive-fg.png similarity index 100% rename from auth/assets/icon-light-adaptive-fg.png rename to auth/assets/generation-icons/icon-light-adaptive-fg.png diff --git a/auth/assets/icon-light.png b/auth/assets/generation-icons/icon-light.png similarity index 100% rename from auth/assets/icon-light.png rename to auth/assets/generation-icons/icon-light.png diff --git a/auth/assets/icons/auth-icon.ico b/auth/assets/icons/auth-icon.ico new file mode 100644 index 000000000..38bb22bcf Binary files /dev/null and b/auth/assets/icons/auth-icon.ico differ diff --git a/auth/assets/icons/auth-icon.png b/auth/assets/icons/auth-icon.png new file mode 100644 index 000000000..3db75740b Binary files /dev/null and b/auth/assets/icons/auth-icon.png differ diff --git a/auth/distribute_options.yaml b/auth/distribute_options.yaml new file mode 100644 index 000000000..9634f4cbd --- /dev/null +++ b/auth/distribute_options.yaml @@ -0,0 +1,25 @@ +output: dist/ + +releases: + - name: dev + jobs: + - name: release-dev-linux-zip + package: + platform: linux + target: zip + - name: release-dev-linux-deb + package: + platform: linux + target: deb + - name: release-dev-linux-appimage + package: + platform: linux + target: appimage + - name: release-dev-windows-exe + package: + platform: windows + target: exe + - name: release-dev-macos-dmg + package: + platform: macos + target: dmg diff --git a/auth/fastlane/metadata/android/en-US/full_description.txt b/auth/fastlane/metadata/android/en-US/full_description.txt index fbab88c75..0db377761 100644 --- a/auth/fastlane/metadata/android/en-US/full_description.txt +++ b/auth/fastlane/metadata/android/en-US/full_description.txt @@ -37,4 +37,4 @@ file, that adheres to the above format. SUPPORT If you need help, please reach out to support@ente.io, and a human will get in touch with you. -If you have feature requests, please create an issue @ https://github.com/ente-io/auth +If you have feature requests, please create an issue @ https://github.com/ente-io/ente diff --git a/auth/fastlane/metadata/android/en-US/title.txt b/auth/fastlane/metadata/android/en-US/title.txt index f1c54762a..257e3ddcc 100644 --- a/auth/fastlane/metadata/android/en-US/title.txt +++ b/auth/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -ente Authenticator \ No newline at end of file +Ente Authenticator \ No newline at end of file diff --git a/auth/fdroid_flutter_icons.yaml b/auth/fdroid_flutter_icons.yaml index da327160a..0ef87effd 100644 --- a/auth/fdroid_flutter_icons.yaml +++ b/auth/fdroid_flutter_icons.yaml @@ -1,6 +1,6 @@ -flutter_icons: - android: "launcher_icon" - image_path: "assets/icon-light.png" - adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png" - adaptive_icon_background: "#ffffff" - +flutter_icons: + android: "launcher_icon" + image_path: "assets/generation-icons/icon-light.png" + adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png" + adaptive_icon_background: "#ffffff" + diff --git a/auth/ios/Podfile.lock b/auth/ios/Podfile.lock index 9e781fd48..aec7e14f0 100644 --- a/auth/ios/Podfile.lock +++ b/auth/ios/Podfile.lock @@ -1,7 +1,9 @@ PODS: - - connectivity (0.0.1): + - app_links (0.0.1): - Flutter - - Reachability + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -45,27 +47,26 @@ PODS: - Flutter (1.0.0) - flutter_email_sender (0.0.1): - Flutter - - flutter_inappwebview (0.0.1): + - flutter_inappwebview_ios (0.0.1): - Flutter - - flutter_inappwebview/Core (= 0.0.1) + - flutter_inappwebview_ios/Core (= 0.0.1) - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) + - flutter_local_authentication (1.2.0): + - Flutter - flutter_local_notifications (0.0.1): - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter - - flutter_sodium (0.0.1): - - Flutter - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) + - local_auth_darwin (0.0.1): + - Flutter - local_auth_ios (0.0.1): - Flutter - move_to_background (0.0.1): @@ -82,46 +83,65 @@ PODS: - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - Reachability (3.2) - - SDWebImage (5.17.0): - - SDWebImage/Core (= 5.17.0) - - SDWebImage/Core (5.17.0) - - Sentry/HybridSDK (8.9.1): - - SentryPrivate (= 8.9.1) + - ReachabilitySwift (5.2.1) + - SDWebImage (5.19.0): + - SDWebImage/Core (= 5.19.0) + - SDWebImage/Core (5.19.0) + - Sentry/HybridSDK (8.21.0): + - SentryPrivate (= 8.21.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.9.1) - - SentryPrivate (8.9.1) + - Sentry/HybridSDK (= 8.21.0) + - SentryPrivate (8.21.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - smart_auth (0.0.1): + - Flutter + - sodium_libs (2.2.1): + - Flutter - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) - - SwiftyGif (5.4.4) - - Toast (4.0.0) - - uni_links (0.0.1): + - FlutterMacOS + - sqlite3 (3.45.1): + - sqlite3/common (= 3.45.1) + - sqlite3/common (3.45.1) + - sqlite3/fts5 (3.45.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.45.1): + - sqlite3/common + - sqlite3/rtree (3.45.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): - Flutter + - sqlite3 (~> 3.45.1) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - SwiftyGif (5.4.4) + - Toast (4.1.0) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: - - connectivity (from `.symlinks/plugins/connectivity/ios`) + - app_links (from `.symlinks/plugins/app_links/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`) - Flutter (from `Flutter`) - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -131,27 +151,31 @@ DEPENDENCIES: - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) - - uni_links (from `.symlinks/plugins/uni_links/ios`) + - smart_auth (from `.symlinks/plugins/smart_auth/ios`) + - sodium_libs (from `.symlinks/plugins/sodium_libs/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - FMDB - MTBBarcodeScanner - OrderedSet - - Reachability + - ReachabilitySwift - SDWebImage - Sentry - SentryPrivate + - sqlite3 - SwiftyGif - Toast EXTERNAL SOURCES: - connectivity: - :path: ".symlinks/plugins/connectivity/ios" + app_links: + :path: ".symlinks/plugins/app_links/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -164,18 +188,20 @@ EXTERNAL SOURCES: :path: Flutter flutter_email_sender: :path: ".symlinks/plugins/flutter_email_sender/ios" - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_local_authentication: + :path: ".symlinks/plugins/flutter_local_authentication/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" - flutter_sodium: - :path: ".symlinks/plugins/flutter_sodium/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" move_to_background: @@ -194,50 +220,58 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + smart_auth: + :path: ".symlinks/plugins/smart_auth/ios" + sodium_libs: + :path: ".symlinks/plugins/sodium_libs/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" - uni_links: - :path: ".symlinks/plugins/uni_links/ios" + :path: ".symlinks/plugins/sqflite/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b - flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62 - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 + local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 - Sentry: e3203780941722a1fcfee99e351de14244c7f806 - sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c - SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13 + ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 + SDWebImage: 981fd7e860af070920f249fd092420006014c3eb + Sentry: ebc12276bd17613a114ab359074096b6b3725203 + sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e + SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 + sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 + sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + Toast: ec33c32b8688982cecc6348adeae667c1b9938da + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb diff --git a/auth/ios/Runner.xcodeproj/project.pbxproj b/auth/ios/Runner.xcodeproj/project.pbxproj index 219a8441c..4d77182b0 100644 --- a/auth/ios/Runner.xcodeproj/project.pbxproj +++ b/auth/ios/Runner.xcodeproj/project.pbxproj @@ -159,7 +159,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db2..5e31d3d34 100644 --- a/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) + + super.application(application, didFinishLaunchingWithOptions: launchOptions) + + if let url = AppLinks.shared.getLink(launchOptions: launchOptions) { + AppLinks.shared.handleLink(url: url) + } + + return false + + // return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } diff --git a/auth/ios/Runner/Info.plist b/auth/ios/Runner/Info.plist index f3e756539..35921ba0c 100644 --- a/auth/ios/Runner/Info.plist +++ b/auth/ios/Runner/Info.plist @@ -78,5 +78,9 @@ UIViewControllerBasedStatusBarAppearance + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + diff --git a/auth/lib/app/view/app.dart b/auth/lib/app/view/app.dart index b723e54b9..3bd9d4e73 100644 --- a/auth/lib/app/view/app.dart +++ b/auth/lib/app/view/app.dart @@ -12,15 +12,18 @@ import 'package:ente_auth/locale.dart'; import "package:ente_auth/onboarding/view/onboarding_page.dart"; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/settings/app_update_dialog.dart'; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; class App extends StatefulWidget { final Locale locale; - const App({Key? key, this.locale = const Locale("en")}) : super(key: key); + const App({super.key, this.locale = const Locale("en")}); static void setLocale(BuildContext context, Locale newLocale) { _AppState state = context.findAncestorStateOfType<_AppState>()!; @@ -31,7 +34,7 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WindowListener, TrayListener { late StreamSubscription _signedOutEvent; late StreamSubscription _signedInEvent; Locale? locale; @@ -41,8 +44,19 @@ class _AppState extends State { }); } + Future initWindowManager() async { + windowManager.addListener(this); + } + + Future initTrayManager() async { + trayManager.addListener(this); + } + @override void initState() { + initWindowManager(); + initTrayManager(); + _signedOutEvent = Bus.instance.on().listen((event) { if (mounted) { setState(() {}); @@ -76,6 +90,10 @@ class _AppState extends State { @override void dispose() { super.dispose(); + + windowManager.removeListener(this); + trayManager.removeListener(this); + _signedOutEvent.cancel(); _signedInEvent.cancel(); } @@ -134,4 +152,45 @@ class _AppState extends State { : const OnboardingPage(), }; } + + @override + void onWindowResize() { + WindowListenerService.instance.onWindowResize().ignore(); + } + + @override + void onTrayIconMouseDown() { + if (Platform.isWindows) { + windowManager.show(); + } else { + trayManager.popUpContextMenu(); + } + } + + @override + void onTrayIconRightMouseDown() { + if (Platform.isWindows) { + trayManager.popUpContextMenu(); + } else { + windowManager.show(); + } + } + + @override + void onTrayIconRightMouseUp() {} + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + switch (menuItem.key) { + case 'hide_window': + windowManager.hide(); + break; + case 'show_window': + windowManager.show(); + break; + case 'exit_app': + windowManager.close(); + break; + } + } } diff --git a/auth/lib/app/view/app_route.dart b/auth/lib/app/view/app_route.dart deleted file mode 100644 index b3cf7bb42..000000000 --- a/auth/lib/app/view/app_route.dart +++ /dev/null @@ -1,4 +0,0 @@ -class AppRoute { - static const String enterSecretKeyPage = "enterSecretKeyPage"; - static const String settings = "settings"; -} diff --git a/auth/lib/app/view/app_theme_extension.dart b/auth/lib/app/view/app_theme_extension.dart deleted file mode 100644 index 548be3d4f..000000000 --- a/auth/lib/app/view/app_theme_extension.dart +++ /dev/null @@ -1,163 +0,0 @@ -import "package:flutter/material.dart"; - -final lightTheme = ThemeData( - fontFamily: "Inter", - brightness: Brightness.light, - scaffoldBackgroundColor: Colors.white, - appBarTheme: const AppBarTheme().copyWith( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - iconTheme: const IconThemeData(color: Colors.black), - elevation: 0, - ), - colorScheme: const ColorScheme.light( - primary: Colors.black, - ), - textTheme: _buildTextTheme(Colors.black), - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.black, - fgDisabled: Colors.white, - fgEnabled: Colors.white, - ), - inputDecorationTheme: InputDecorationTheme( - fillColor: null, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), -); - -final darkTheme = ThemeData( - fontFamily: "Inter", - brightness: Brightness.dark, - scaffoldBackgroundColor: Colors.black, - appBarTheme: const AppBarTheme(color: Colors.orange), - textTheme: _buildTextTheme(Colors.white), - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.white, - fgDisabled: Colors.white, - fgEnabled: Colors.black, - ), - inputDecorationTheme: InputDecorationTheme( - fillColor: null, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: Colors.black), -); - -TextTheme _buildTextTheme(Color textColor) { - return const TextTheme().copyWith( - headlineMedium: TextStyle( - color: textColor, - fontSize: 32, - fontWeight: FontWeight.w700, - fontFamily: "Inter", - ), - headlineSmall: TextStyle( - color: textColor, - fontSize: 24, - fontWeight: FontWeight.w600, - fontFamily: "Inter", - ), - // AG: Body - titleLarge: TextStyle( - color: textColor, - fontSize: 18, - fontFamily: "Inter", - fontWeight: FontWeight.w500, - ), - // Use labels for buttons or notifications - labelMedium: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 18, - fontWeight: FontWeight.w700, - height: 28, - ), - - titleMedium: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 16, - fontWeight: FontWeight.w500, - ), - titleSmall: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 14, - fontWeight: FontWeight.w500, - ), - bodyLarge: TextStyle( - fontFamily: "Inter", - color: textColor, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - bodyMedium: TextStyle( - fontFamily: "Inter", - color: textColor, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - bodySmall: TextStyle( - color: textColor.withOpacity(0.6), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ); -} - -OutlinedButtonThemeData buildOutlinedButtonThemeData({ - required Color bgDisabled, - required Color bgEnabled, - required Color fgDisabled, - required Color fgEnabled, -}) { - return OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 50), - textStyle: const TextStyle( - fontWeight: FontWeight.w700, - fontFamily: "Inter", - fontSize: 18, - ), - ).copyWith( - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { - return bgDisabled; - } - return bgEnabled; - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { - return fgDisabled; - } - return fgEnabled; - }, - ), - alignment: Alignment.center, - ), - ); -} diff --git a/auth/lib/bootstrap.dart b/auth/lib/bootstrap.dart index e423ba7c8..5f774e397 100644 --- a/auth/lib/bootstrap.dart +++ b/auth/lib/bootstrap.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import "dart:async"; import "dart:developer"; diff --git a/auth/lib/core/configuration.dart b/auth/lib/core/configuration.dart index da3ad9ebf..096fe91b2 100644 --- a/auth/lib/core/configuration.dart +++ b/auth/lib/core/configuration.dart @@ -13,12 +13,12 @@ import 'package:ente_auth/models/key_attributes.dart'; 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/crypto_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:tuple/tuple.dart'; class Configuration { @@ -72,9 +72,10 @@ class Configuration { Future init() async { _preferences = await SharedPreferences.getInstance(); + sqfliteFfiInit(); _secureStorage = const FlutterSecureStorage(); _documentsDirectory = (await getApplicationDocumentsDirectory()).path; - _tempDirectory = _documentsDirectory + "/temp/"; + _tempDirectory = "$_documentsDirectory/temp/"; final tempDirectory = io.Directory(_tempDirectory); try { final currentTime = DateTime.now().microsecondsSinceEpoch; @@ -162,7 +163,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -172,28 +173,28 @@ class Configuration { CryptoUtil.encryptSync(masterKey, derivedKeyResult.key); // Generate a public-private keypair and encrypt the latter - final keyPair = await CryptoUtil.generateKeyPair(); + final keyPair = CryptoUtil.generateKeyPair(); final encryptedSecretKeyData = - CryptoUtil.encryptSync(keyPair.sk, masterKey); + CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey); final attributes = KeyAttributes( - Sodium.bin2base64(kekSalt), - Sodium.bin2base64(encryptedKeyData.encryptedData!), - Sodium.bin2base64(encryptedKeyData.nonce!), - Sodium.bin2base64(keyPair.pk), - Sodium.bin2base64(encryptedSecretKeyData.encryptedData!), - Sodium.bin2base64(encryptedSecretKeyData.nonce!), + CryptoUtil.bin2base64(kekSalt), + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedKeyData.nonce!), + CryptoUtil.bin2base64(keyPair.publicKey), + CryptoUtil.bin2base64(encryptedSecretKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedSecretKeyData.nonce!), derivedKeyResult.memLimit, derivedKeyResult.opsLimit, - Sodium.bin2base64(encryptedMasterKey.encryptedData!), - Sodium.bin2base64(encryptedMasterKey.nonce!), - Sodium.bin2base64(encryptedRecoveryKey.encryptedData!), - Sodium.bin2base64(encryptedRecoveryKey.nonce!), + CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!), + CryptoUtil.bin2base64(encryptedMasterKey.nonce!), + CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!), + CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!), ); final privateAttributes = PrivateKeyAttributes( - Sodium.bin2base64(masterKey), - Sodium.bin2hex(recoveryKey), - Sodium.bin2base64(keyPair.sk), + CryptoUtil.bin2base64(masterKey), + CryptoUtil.bin2hex(recoveryKey), + CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()), ); return KeyGenResult(attributes, privateAttributes, loginKey); } @@ -208,7 +209,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -220,9 +221,9 @@ class Configuration { final existingAttributes = getKeyAttributes(); final updatedAttributes = existingAttributes!.copyWith( - kekSalt: Sodium.bin2base64(kekSalt), - encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!), - keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!), + kekSalt: CryptoUtil.bin2base64(kekSalt), + encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!), memLimit: derivedKeyResult.memLimit, opsLimit: derivedKeyResult.opsLimit, ); @@ -240,8 +241,8 @@ class Configuration { }) async { _logger.info('Start decryptAndSaveSecrets'); keyEncryptionKey ??= await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, - Sodium.base642bin(attributes.kekSalt), + utf8.encode(password), + CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit, attributes.opsLimit, ); @@ -250,31 +251,31 @@ class Configuration { Uint8List key; try { key = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedKey), + CryptoUtil.base642bin(attributes.encryptedKey), keyEncryptionKey, - Sodium.base642bin(attributes.keyDecryptionNonce), + CryptoUtil.base642bin(attributes.keyDecryptionNonce), ); } catch (e) { _logger.severe('master-key failed, incorrect password?', e); throw Exception("Incorrect password"); } _logger.info("master-key done"); - await setKey(Sodium.bin2base64(key)); + await setKey(CryptoUtil.bin2base64(key)); final secretKey = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedSecretKey), + CryptoUtil.base642bin(attributes.encryptedSecretKey), key, - Sodium.base642bin(attributes.secretKeyDecryptionNonce), + CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce), ); _logger.info("secret-key done"); - await setSecretKey(Sodium.bin2base64(secretKey)); + await setSecretKey(CryptoUtil.bin2base64(secretKey)); final token = CryptoUtil.openSealSync( - Sodium.base642bin(getEncryptedToken()!), - Sodium.base642bin(attributes.publicKey), + CryptoUtil.base642bin(getEncryptedToken()!), + CryptoUtil.base642bin(attributes.publicKey), secretKey, ); _logger.info('appToken done'); await setToken( - Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe), + CryptoUtil.bin2base64(token, urlSafe: true), ); return keyEncryptionKey; } @@ -293,28 +294,28 @@ class Configuration { Uint8List masterKey; try { masterKey = await CryptoUtil.decrypt( - Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey), - Sodium.hex2bin(recoveryKey), - Sodium.base642bin(attributes.masterKeyDecryptionNonce), + CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey), + CryptoUtil.hex2bin(recoveryKey), + CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce), ); } catch (e) { _logger.severe(e); rethrow; } - await setKey(Sodium.bin2base64(masterKey)); + await setKey(CryptoUtil.bin2base64(masterKey)); final secretKey = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedSecretKey), + CryptoUtil.base642bin(attributes.encryptedSecretKey), masterKey, - Sodium.base642bin(attributes.secretKeyDecryptionNonce), + CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce), ); - await setSecretKey(Sodium.bin2base64(secretKey)); + await setSecretKey(CryptoUtil.bin2base64(secretKey)); final token = CryptoUtil.openSealSync( - Sodium.base642bin(getEncryptedToken()!), - Sodium.base642bin(attributes.publicKey), + CryptoUtil.base642bin(getEncryptedToken()!), + CryptoUtil.base642bin(attributes.publicKey), secretKey, ); await setToken( - Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe), + CryptoUtil.bin2base64(token, urlSafe: true), ); } @@ -407,27 +408,31 @@ class Configuration { } Uint8List? getKey() { - return _key == null ? null : Sodium.base642bin(_key!); + return _key == null ? null : CryptoUtil.base642bin(_key!); } Uint8List? getSecretKey() { - return _secretKey == null ? null : Sodium.base642bin(_secretKey!); + return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!); } Uint8List? getAuthSecretKey() { - return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!); + return _authSecretKey == null + ? null + : CryptoUtil.base642bin(_authSecretKey!); } Uint8List? getOfflineSecretKey() { - return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!); + return _offlineAuthKey == null + ? null + : CryptoUtil.base642bin(_offlineAuthKey!); } Uint8List getRecoveryKey() { final keyAttributes = getKeyAttributes()!; return CryptoUtil.decryptSync( - Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey), + CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey), getKey()!, - Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce), + CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce), ); } @@ -454,7 +459,7 @@ class Configuration { iOptions: _secureStorageOptionsIOS, ); } else { - _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey()); + _offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey()); await _secureStorage.write( key: offlineAuthSecretKey, value: _offlineAuthKey, diff --git a/auth/lib/core/constants.dart b/auth/lib/core/constants.dart index dcc111c4b..5685220ac 100644 --- a/auth/lib/core/constants.dart +++ b/auth/lib/core/constants.dart @@ -7,7 +7,8 @@ const String sentryDSN = "https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"; const String sentryTunnel = "https://sentry-reporter.ente.io"; const String roadmapURL = "https://roadmap.ente.io"; -const String githubDiscussionsUrl = "https://github.com/ente-io/ente/discussions"; +const String githubIssuesUrl = + "https://github.com/ente-io/ente/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc"; const int microSecondsInDay = 86400000000; const int android11SDKINT = 30; const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748 diff --git a/auth/lib/core/errors.dart b/auth/lib/core/errors.dart index c1e83cde0..ba1310b6c 100644 --- a/auth/lib/core/errors.dart +++ b/auth/lib/core/errors.dart @@ -1,9 +1,9 @@ class InvalidFileError extends ArgumentError { - InvalidFileError(String message) : super(message); + InvalidFileError(String super.message); } class InvalidFileUploadState extends AssertionError { - InvalidFileUploadState(String message) : super(message); + InvalidFileUploadState(String super.message); } class SubscriptionAlreadyClaimedError extends Error {} @@ -30,19 +30,15 @@ class UnauthorizedError extends Error {} class RequestCancelledError extends Error {} class InvalidSyncStatusError extends AssertionError { - InvalidSyncStatusError(String message) : super(message); + InvalidSyncStatusError(String super.message); } class UnauthorizedEditError extends AssertionError {} class InvalidStateError extends AssertionError { - InvalidStateError(String message) : super(message); + InvalidStateError(String super.message); } -class KeyDerivationError extends Error {} - -class LoginKeyDerivationError extends Error {} - class SrpSetupNotCompleteError extends Error {} class AuthenticatorKeyNotFound extends Error {} diff --git a/auth/lib/core/logging/super_logging.dart b/auth/lib/core/logging/super_logging.dart index 39a983874..08c3f475e 100644 --- a/auth/lib/core/logging/super_logging.dart +++ b/auth/lib/core/logging/super_logging.dart @@ -235,14 +235,14 @@ class SuperLogging { extraLines = null; } - final str = (config.prefix) + " " + rec.toPrettyString(extraLines); + final str = "${config.prefix} ${rec.toPrettyString(extraLines)}"; // write to stdout printLog(str); // push to log queue if (fileIsEnabled) { - fileQueueEntries.add(str + '\n'); + fileQueueEntries.add('$str\n'); if (fileQueueEntries.length == 1) { flushQueue(); } @@ -275,7 +275,7 @@ class SuperLogging { static var logChunkSize = 800; static void printLog(String text) { - text.chunked(logChunkSize).forEach(print); + text.chunked(logChunkSize).forEach(debugPrint); } /// A queue to be consumed by [setupSentry]. @@ -354,7 +354,7 @@ class SuperLogging { final date = config.dateFmt!.parse(basename(file.path)); dates[file as File] = date; files.add(file); - } on FormatException {} + } on Exception catch (_) {} } final nowTime = DateTime.now(); @@ -374,7 +374,7 @@ class SuperLogging { "deleting log file ${file.path}", ); await file.delete(); - } catch (ignore) {} + } on Exception catch (_) {} } } diff --git a/auth/lib/core/logging/tunneled_transport.dart b/auth/lib/core/logging/tunneled_transport.dart index 1191adc2e..f9ffd5ad1 100644 --- a/auth/lib/core/logging/tunneled_transport.dart +++ b/auth/lib/core/logging/tunneled_transport.dart @@ -46,7 +46,7 @@ class TunneledTransport implements Transport { _options.logger( SentryLevel.error, 'API returned an error, statusCode = ${response.statusCode}, ' - 'body = ${response.body}', + 'body = ${response.body}', ); } return const SentryId.empty(); @@ -65,8 +65,8 @@ class TunneledTransport implements Transport { } Future _createStreamedRequest( - SentryEnvelope envelope, - ) async { + SentryEnvelope envelope, + ) async { final streamedRequest = StreamedRequest('POST', _tunnel); envelope .envelopeStream(_options) @@ -91,10 +91,10 @@ class _CredentialBuilder { _clock = clock; factory _CredentialBuilder( - Dsn? dsn, - String sdkIdentifier, - ClockProvider clock, - ) { + Dsn? dsn, + String sdkIdentifier, + ClockProvider clock, + ) { final authHeader = _buildAuthHeader( publicKey: dsn?.publicKey, secretKey: dsn?.secretKey, diff --git a/auth/lib/core/network.dart b/auth/lib/core/network.dart index 1942fcbb8..c14c9e758 100644 --- a/auth/lib/core/network.dart +++ b/auth/lib/core/network.dart @@ -4,9 +4,10 @@ import 'package:dio/dio.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/endpoint_updated_event.dart'; +import 'package:ente_auth/utils/package_info_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:uuid/uuid.dart'; int kConnectTimeout = 15000; @@ -16,34 +17,41 @@ class Network { late Dio _enteDio; Future init() async { - await FkUserAgent.init(); - final packageInfo = await PackageInfo.fromPlatform(); + if (PlatformUtil.isMobile()) await FkUserAgent.init(); + final packageInfo = await PackageInfoUtil().getPackageInfo(); + final version = PackageInfoUtil().getVersion(packageInfo); + final packageName = PackageInfoUtil().getPackageName(packageInfo); final endpoint = Configuration.instance.getHttpEndpoint(); - + _dio = Dio( BaseOptions( - connectTimeout: kConnectTimeout, + connectTimeout: Duration(milliseconds: kConnectTimeout), headers: { - HttpHeaders.userAgentHeader: FkUserAgent.userAgent, - 'X-Client-Version': packageInfo.version, - 'X-Client-Package': packageInfo.packageName, + HttpHeaders.userAgentHeader: PlatformUtil.isMobile() + ? FkUserAgent.userAgent + : Platform.operatingSystem, + 'X-Client-Version': version, + 'X-Client-Package': packageName, }, ), ); - + _enteDio = Dio( BaseOptions( baseUrl: endpoint, - connectTimeout: kConnectTimeout, + connectTimeout: Duration(milliseconds: kConnectTimeout), headers: { - HttpHeaders.userAgentHeader: FkUserAgent.userAgent, - 'X-Client-Version': packageInfo.version, - 'X-Client-Package': packageInfo.packageName, + if (PlatformUtil.isMobile()) + HttpHeaders.userAgentHeader: FkUserAgent.userAgent + else + HttpHeaders.userAgentHeader: Platform.operatingSystem, + 'X-Client-Version': version, + 'X-Client-Package': packageName, }, ), ); _setupInterceptors(endpoint); - + Bus.instance.on().listen((event) { final endpoint = Configuration.instance.getHttpEndpoint(); _enteDio.options.baseUrl = endpoint; diff --git a/auth/lib/ente_theme_data.dart b/auth/lib/ente_theme_data.dart index bb2a994c4..0316d014f 100644 --- a/auth/lib/ente_theme_data.dart +++ b/auth/lib/ente_theme_data.dart @@ -5,12 +5,16 @@ import 'package:flutter/material.dart'; final lightThemeData = ThemeData( fontFamily: 'Inter', brightness: Brightness.light, + dividerTheme: const DividerThemeData( + color: Colors.black12, + ), hintColor: const Color.fromRGBO(158, 158, 158, 1), primaryColor: const Color.fromRGBO(255, 110, 64, 1), primaryColorLight: const Color.fromRGBO(0, 0, 0, 0.541), iconTheme: const IconThemeData(color: Colors.black), primaryIconTheme: const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), + buttonTheme: const ButtonThemeData(), outlinedButtonTheme: buildOutlinedButtonThemeData( bgDisabled: const Color.fromRGBO(158, 158, 158, 1), bgEnabled: const Color.fromRGBO(0, 0, 0, 1), @@ -72,24 +76,42 @@ final lightThemeData = ThemeData( ? const Color.fromRGBO(255, 255, 255, 1) : const Color.fromRGBO(0, 0, 0, 1); }), - ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - trackColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), colorScheme: const ColorScheme.light( + ), + radioTheme: RadioThemeData( + fillColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + switchTheme: SwitchThemeData( + thumbColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + trackColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + colorScheme: const ColorScheme.light( primary: Colors.black, secondary: Color.fromARGB(255, 163, 163, 163), ).copyWith(background: const Color.fromRGBO(255, 255, 255, 1)), @@ -98,6 +120,9 @@ final lightThemeData = ThemeData( final darkThemeData = ThemeData( fontFamily: 'Inter', brightness: Brightness.dark, + dividerTheme: const DividerThemeData( + color: Colors.white12, + ), primaryColorLight: const Color.fromRGBO(255, 255, 255, 0.702), iconTheme: const IconThemeData(color: Colors.white), primaryIconTheme: @@ -105,6 +130,7 @@ final darkThemeData = ThemeData( hintColor: const Color.fromRGBO(158, 158, 158, 1), buttonTheme: const ButtonThemeData().copyWith( buttonColor: const Color.fromRGBO(45, 194, 98, 1.0), + height: 56, ), textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)), outlinedButtonTheme: buildOutlinedButtonThemeData( @@ -164,24 +190,43 @@ final darkThemeData = ThemeData( return const Color.fromRGBO(158, 158, 158, 1); } }), - ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - trackColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: const Color.fromRGBO(0, 0, 0, 1)), + ), + radioTheme: RadioThemeData( + fillColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + switchTheme: SwitchThemeData( + thumbColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + trackColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + colorScheme: const ColorScheme.dark(primary: Colors.white) + .copyWith(background: const Color.fromRGBO(0, 0, 0, 1)), ); TextTheme _buildTextTheme(Color textColor) { @@ -400,6 +445,7 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({ shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), + fixedSize: const Size.fromHeight(56), alignment: Alignment.center, padding: const EdgeInsets.fromLTRB(50, 16, 50, 16), textStyle: const TextStyle( @@ -436,7 +482,9 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({ }) { return ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation, + foregroundColor: onPrimary, + backgroundColor: primary, + elevation: elevation, alignment: Alignment.center, textStyle: const TextStyle( fontWeight: FontWeight.w600, diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index ee19c79b0..3f3c7c897 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -25,7 +25,7 @@ class AuthenticatorGateway { try { final response = await _enteDio.get("/authenticator/key"); return AuthKey.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && (e.response!.statusCode ?? 0) == 404) { throw AuthenticatorKeyNotFound(); } else { @@ -90,7 +90,7 @@ class AuthenticatorGateway { } return authEntities; } catch (e) { - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } else { rethrow; diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 32507899c..7b21fcab1 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -145,6 +145,7 @@ "lostDeviceTitle": "Gerät verloren?", "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung", "passkeyAuthTitle": "Passkey Authentifizierung", + "verifyPasskey": "Passkey verifizieren", "recoverAccount": "Konto wiederherstellen", "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein", "recover": "Wiederherstellen", @@ -407,6 +408,7 @@ "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)", "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "waitingForBrowserRequest": "Warten auf Browseranfrage...", + "waitingForVerification": "Warte auf Bestätigung...", "passkey": "Passkey", "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?", "developerSettings": "Entwicklereinstellungen", diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index f540fc717..a2f6e77ed 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -199,6 +199,10 @@ "recoveryKeySaveDescription": "We don't store this key, please save this 24 word key in a safe place.", "doThisLater": "Do this later", "saveKey": "Save key", + "save": "Save", + "send": "Send", + "saveOrSendDescription": "Do you want to save this to your storage (Downloads folder by default) or send it to other apps?", + "saveOnlyDescription": "Do you want to save this to your storage (Downloads folder by default)?", "back": "Back", "createAccount": "Create account", "passwordStrength": "Password strength: {passwordStrengthValue}", @@ -407,6 +411,7 @@ "doNotSignOut": "Do not sign out", "hearUsWhereTitle": "How did you hear about Ente? (optional)", "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!", + "recoveryKeySaved": "Recovery key saved in Downloads folder!", "waitingForBrowserRequest": "Waiting for browser request...", "waitingForVerification": "Waiting for verification...", "passkey": "Passkey", diff --git a/auth/lib/l10n/arb/app_ko.arb b/auth/lib/l10n/arb/app_ko.arb new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/auth/lib/l10n/arb/app_ko.arb @@ -0,0 +1 @@ +{} \ 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 0ac2b3791..ad1728204 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -144,6 +144,8 @@ "enterCodeHint": "Voer de 6-cijferige code van je verificatie-app in", "lostDeviceTitle": "Apparaat verloren?", "twoFactorAuthTitle": "Tweestapsverificatie", + "passkeyAuthTitle": "Passkey verificatie", + "verifyPasskey": "Bevestig passkey", "recoverAccount": "Account herstellen", "enterRecoveryKeyHint": "Voer je herstelsleutel in", "recover": "Herstellen", @@ -404,5 +406,15 @@ "signOutOtherDevices": "Afmelden bij andere apparaten", "doNotSignOut": "Niet uitloggen", "hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)", - "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!" + "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", + "waitingForBrowserRequest": "Wachten op browserverzoek...", + "waitingForVerification": "Wachten op verificatie...", + "passkey": "Passkey", + "developerSettingsWarning": "Weet u zeker dat u de ontwikkelaarsinstellingen wilt wijzigen?", + "developerSettings": "Ontwikkelaarsinstellingen", + "serverEndpoint": "Server eindpunt", + "invalidEndpoint": "Ongeldig eindpunt", + "invalidEndpointMessage": "Sorry, het eindpunt dat u hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw.", + "endpointUpdatedMessage": "Eindpunt met succes bijgewerkt", + "customEndpoint": "Verbonden met {endpoint}" } \ 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 f36c690f7..3221799d0 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -338,5 +338,22 @@ "deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod", "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR", "confirmAccountDeleteTitle": "Potwierdź usunięcie konta", - "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte." + "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte.", + "androidBiometricNotRecognized": "Nie rozpoznano. Spróbuj ponownie.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidSignInTitle": "Wymagana autoryzacja", + "@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." + }, + "goToSettings": "Przejdź do Ustawień", + "@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." + }, + "noInternetConnection": "Brak połączenia z Internetem", + "pleaseCheckYourInternetConnectionAndTryAgain": "Proszę sprawdzić połączenie internetowe i spróbować ponownie.", + "hearUsWhereTitle": "Jak usłyszałeś o Ente? (opcjonalnie)", + "waitingForVerification": "Oczekiwanie na weryfikację...", + "developerSettings": "Ustawienia deweloperskie" } \ 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 aa6a2a837..e6ca32893 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -47,9 +47,9 @@ }, "copyEmailAction": "Copiar e-mail", "exportLogsAction": "Exportar logs", - "reportABug": "Reportar um bug", + "reportABug": "Reportar um problema", "crashAndErrorReporting": "Reporte de erros e falhas", - "reportBug": "Reportar bug", + "reportBug": "Reportar problema", "emailUsMessage": "Por favor, envie um e-mail para {email}", "@emailUsMessage": { "placeholders": { @@ -145,6 +145,7 @@ "lostDeviceTitle": "Perdeu seu dispositivo?", "twoFactorAuthTitle": "Autenticação de dois fatores", "passkeyAuthTitle": "Autenticação via Chave de acesso", + "verifyPasskey": "Verificar chave de acesso", "recoverAccount": "Recuperar conta", "enterRecoveryKeyHint": "Digite sua chave de recuperação", "recover": "Recuperar", @@ -162,7 +163,7 @@ "invalidEmailMessage": "Por favor, insira um endereço de e-mail válido.", "deleteAccount": "Excluir conta", "deleteAccountQuery": "Sentiremos muito por vê-lo partir. Você está enfrentando algum problema?", - "yesSendFeedbackAction": "Sim, enviar feedback", + "yesSendFeedbackAction": "Sim, enviar comentário", "noDeleteAccountAction": "Não, excluir conta", "initiateAccountDeleteTitle": "Por favor, autentique-se para iniciar a exclusão de conta", "sendEmail": "Enviar e-mail", @@ -407,6 +408,7 @@ "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)", "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "waitingForBrowserRequest": "Aguardando solicitação do navegador...", + "waitingForVerification": "Esperando por verificação...", "passkey": "Chave de acesso", "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?", "developerSettings": "Configurações de desenvolvedor", diff --git a/auth/lib/l10n/arb/app_zh.arb b/auth/lib/l10n/arb/app_zh.arb index e07e7dcfa..5328a14b9 100644 --- a/auth/lib/l10n/arb/app_zh.arb +++ b/auth/lib/l10n/arb/app_zh.arb @@ -145,6 +145,7 @@ "lostDeviceTitle": "丢失了设备吗?", "twoFactorAuthTitle": "双因素认证", "passkeyAuthTitle": "通行密钥认证", + "verifyPasskey": "验证通行密钥", "recoverAccount": "恢复账户", "enterRecoveryKeyHint": "输入您的恢复密钥", "recover": "恢复", @@ -407,6 +408,7 @@ "hearUsWhereTitle": "您是如何知道Ente的? (可选的)", "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "waitingForBrowserRequest": "正在等待浏览器请求...", + "waitingForVerification": "等待验证...", "passkey": "通行密钥", "developerSettingsWarning": "您确定要修改开发者设置吗?", "developerSettings": "开发者设置", diff --git a/auth/lib/main.dart b/auth/lib/main.dart index 02d3e0c12..09b85d8b3 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; -import 'package:computer/computer.dart'; import "package:ente_auth/app/view/app.dart"; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/constants.dart'; @@ -17,11 +16,14 @@ import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/services/user_remote_flag_service.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/ui/tools/lock_screen.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/window_protocol_handler.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import 'package:flutter/scheduler.dart'; @@ -29,11 +31,52 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:privacy_screen/privacy_screen.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; final _logger = Logger("main"); +Future initSystemTray() async { + String path = Platform.isWindows + ? 'assets/icons/auth-icon.ico' + : 'assets/icons/auth-icon.png'; + await trayManager.setIcon(path); + Menu menu = Menu( + items: [ + MenuItem( + key: 'hide_window', + label: 'Hide Window', + ), + MenuItem( + key: 'show_window', + label: 'Show Window', + ), + MenuItem.separator(), + MenuItem( + key: 'exit_app', + label: 'Exit App', + ), + ], + ); + await trayManager.setContextMenu(menu); +} + void main() async { WidgetsFlutterBinding.ensureInitialized(); + + initSystemTray().ignore(); + + if (PlatformUtil.isDesktop()) { + await windowManager.ensureInitialized(); + await WindowListenerService.instance.init(); + WindowOptions windowOptions = WindowOptions( + size: WindowListenerService.instance.getWindowSize(), + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + } await _runInForeground(); await _setupPrivacyScreen(); if (Platform.isAndroid) { @@ -70,10 +113,14 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) { } Future _runWithLogs(Function() function, {String prefix = ""}) async { + String dir = ""; + try { + dir = "${(await getApplicationSupportDirectory()).path}/logs"; + } catch (_) {} await SuperLogging.main( LogConfig( body: function, - logDirPath: (await getApplicationSupportDirectory()).path + "/logs", + logDirPath: dir, maxLogFiles: 5, sentryDsn: sentryDSN, enableInDebugMode: true, @@ -82,10 +129,19 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async { ); } +void _registerWindowsProtocol() { + const kWindowsScheme = 'ente'; + // Register our protocol only on Windows platform + if (!kIsWeb && Platform.isWindows) { + WindowsProtocolHandler() + .register(kWindowsScheme, executable: null, arguments: null); + } +} + Future _init(bool bool, {String? via}) async { - // Start workers asynchronously. No need to wait for them to start - Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode).ignore(); - CryptoUtil.init(); + _registerWindowsProtocol(); + await initCryptoUtil(); + await PreferenceService.instance.init(); await CodeStore.instance.init(); await Configuration.instance.init(); @@ -100,6 +156,7 @@ Future _init(bool bool, {String? via}) async { } Future _setupPrivacyScreen() async { + if (!PlatformUtil.isMobile()) return; final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; bool isInDarkMode = brightness == Brightness.dark; diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index 40c8c6455..7853eb19d 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -57,14 +57,7 @@ class Code { updatedAlgo, updatedType, updatedCounter, - "otpauth://${updatedType.name}/" + - updateIssuer + - ":" + - updateAccount + - "?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=" + - updateIssuer + - "&period=$updatePeriod&secret=" + - updatedSecret + (updatedType == Type.hotp ? "&counter=$updatedCounter" : ""), + "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", generatedID: generatedID, ); } @@ -83,35 +76,28 @@ class Code { Algorithm.sha1, Type.totp, 0, - "otpauth://totp/" + - issuer + - ":" + - account + - "?algorithm=SHA1&digits=6&issuer=" + - issuer + - "&period=30&secret=" + - secret, + "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret", ); } static Code fromRawData(String rawData) { Uri uri = Uri.parse(rawData); try { - return Code( - _getAccount(uri), - _getIssuer(uri), - _getDigits(uri), - _getPeriod(uri), - getSanitizedSecret(uri.queryParameters['secret']!), - _getAlgorithm(uri), - _getType(uri), - _getCounter(uri), - rawData, - ); - } catch(e) { + return Code( + _getAccount(uri), + _getIssuer(uri), + _getDigits(uri), + _getPeriod(uri), + getSanitizedSecret(uri.queryParameters['secret']!), + _getAlgorithm(uri), + _getType(uri), + _getCounter(uri), + rawData, + ); + } catch (e) { // if account name contains # without encoding, // rest of the url are treated as url fragment - if(rawData.contains("#")) { + if (rawData.contains("#")) { return Code.fromRawData(rawData.replaceAll("#", '%23')); } else { rethrow; @@ -141,7 +127,7 @@ class Code { if (uri.queryParameters.containsKey("issuer")) { String issuerName = uri.queryParameters['issuer']!; // Handle issuer name with period - // See https://github.com/ente-io/auth/pull/77 + // See https://github.com/ente-io/ente/pull/77 if (issuerName.contains("period=")) { return issuerName.substring(0, issuerName.indexOf("period=")); } diff --git a/auth/lib/models/derived_key_result.dart b/auth/lib/models/derived_key_result.dart deleted file mode 100644 index a071fb1f8..000000000 --- a/auth/lib/models/derived_key_result.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:typed_data'; - -class DerivedKeyResult { - final Uint8List key; - final int memLimit; - final int opsLimit; - - DerivedKeyResult(this.key, this.memLimit, this.opsLimit); -} diff --git a/auth/lib/models/encryption_result.dart b/auth/lib/models/encryption_result.dart deleted file mode 100644 index 9da16c573..000000000 --- a/auth/lib/models/encryption_result.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:typed_data'; - -class EncryptionResult { - final Uint8List? encryptedData; - final Uint8List? key; - final Uint8List? header; - final Uint8List? nonce; - - EncryptionResult({ - this.encryptedData, - this.key, - this.header, - this.nonce, - }); -} diff --git a/auth/lib/onboarding/view/onboarding_page.dart b/auth/lib/onboarding/view/onboarding_page.dart index b97c3436a..a0bc6fb66 100644 --- a/auth/lib/onboarding/view/onboarding_page.dart +++ b/auth/lib/onboarding/view/onboarding_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:ente_auth/app/view/app.dart'; import 'package:ente_auth/core/configuration.dart'; @@ -28,7 +29,7 @@ import "package:flutter/material.dart"; import 'package:local_auth/local_auth.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key? key}) : super(key: key); + const OnboardingPage({super.key}); @override State createState() => _OnboardingPageState(); @@ -86,118 +87,128 @@ class _OnboardingPageState extends State { } } }, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), - child: Column( - children: [ - Column( - children: [ - kDebugMode - ? GestureDetector( - child: const Align( - alignment: Alignment.topRight, - child: Text("Lang"), - ), - onTap: () async { - final locale = await getLocale(); - // ignore: unawaited_futures - routeToPage( - context, - LanguageSelectorPage( - appSupportedLocales, - (locale) async { - await setLocale(locale); - App.setLocale(context, locale); - }, - locale, - ), - ).then((value) { - setState(() {}); - }); - }, - ) - : const SizedBox(), - Image.asset( - "assets/sheild-front-gradient.png", - width: 200, - height: 200, - ), - const SizedBox(height: 12), - const Text( - "ente", - style: TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'Montserrat', - fontSize: 42, - ), - ), - const SizedBox(height: 4), - Text( - "Authenticator", - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 32), - Text( - l10n.onBoardingBody, - textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.titleLarge!.copyWith( - color: Colors.white38, + child: SingleChildScrollView( + child: Center( + child: ConstrainedBox( + constraints: + const BoxConstraints.tightFor(height: 800, width: 450), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 40.0, + horizontal: 40, + ), + child: Column( + children: [ + Column( + children: [ + kDebugMode + ? GestureDetector( + child: const Align( + alignment: Alignment.topRight, + child: Text("Lang"), ), - ), - ], - ), - const SizedBox(height: 100), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 20), - child: GradientButton( - onTap: _navigateToSignUpPage, - text: l10n.newUser, + onTap: () async { + final locale = await getLocale(); + // ignore: unawaited_futures + routeToPage( + context, + LanguageSelectorPage( + appSupportedLocales, + (locale) async { + await setLocale(locale); + App.setLocale(context, locale); + }, + locale, + ), + ).then((value) { + setState(() {}); + }); + }, + ) + : const SizedBox(), + Image.asset( + "assets/sheild-front-gradient.png", + width: 200, + height: 200, + ), + const SizedBox(height: 12), + const Text( + "ente", + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'Montserrat', + fontSize: 42, + ), + ), + const SizedBox(height: 4), + Text( + "Authenticator", + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 32), + Text( + l10n.onBoardingBody, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + color: Colors.white38, + ), + ), + ], ), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), - child: Hero( - tag: "log_in", - child: ElevatedButton( - style: Theme.of(context) - .colorScheme - .optionalActionButtonStyle, - onPressed: _navigateToSignInPage, - child: Text( - l10n.existingUser, - style: const TextStyle( - color: Colors.black, // same for both themes + const SizedBox(height: 100), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: GradientButton( + onTap: _navigateToSignUpPage, + text: l10n.newUser, + ), + ), + const SizedBox(height: 16), + Container( + height: 56, + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Hero( + tag: "log_in", + child: ElevatedButton( + style: Theme.of(context) + .colorScheme + .optionalActionButtonStyle, + onPressed: _navigateToSignInPage, + child: Text( + l10n.existingUser, + style: const TextStyle( + color: Colors.black, // same for both themes + ), ), ), ), ), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.only(top: 20, bottom: 20), - child: GestureDetector( - onTap: _optForOfflineMode, - child: Center( - child: Text( - l10n.useOffline, - style: body.copyWith( - color: - Theme.of(context).colorScheme.mutedTextColor, + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: GestureDetector( + onTap: _optForOfflineMode, + child: Center( + child: Text( + l10n.useOffline, + style: body.copyWith( + color: Theme.of(context) + .colorScheme + .mutedTextColor, + ), ), ), ), ), - ), - const DeveloperSettingsWidget(), - ], + const DeveloperSettingsWidget(), + ], + ), ), ), ), @@ -208,7 +219,9 @@ class _OnboardingPageState extends State { } Future _optForOfflineMode() async { - bool canCheckBio = await LocalAuthentication().canCheckBiometrics; + bool canCheckBio = Platform.isMacOS || Platform.isLinux + ? true + : await LocalAuthentication().canCheckBiometrics; if (!canCheckBio) { showToast( context, 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 ee46d7953..3937142d6 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -9,7 +9,7 @@ import "package:flutter/material.dart"; class SetupEnterSecretKeyPage extends StatefulWidget { final Code? code; - SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key); + SetupEnterSecretKeyPage({this.code, super.key}); @override State createState() => @@ -32,7 +32,7 @@ class _SetupEnterSecretKeyPageState extends State { widget.code != null ? safeDecode(widget.code!.account).trim() : null, ); _secretController = TextEditingController( - text: widget.code != null ? widget.code!.secret : null, + text: widget.code?.secret, ); _secretKeyObscured = widget.code != null; super.initState(); @@ -45,8 +45,8 @@ class _SetupEnterSecretKeyPageState extends State { appBar: AppBar( title: Text(l10n.importAccountPageTitle), ), - body: SafeArea( - child: Center( + body: Center( + child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), child: Column( diff --git a/auth/lib/onboarding/view/view_qr_page.dart b/auth/lib/onboarding/view/view_qr_page.dart index 71f4756a7..687f810e3 100644 --- a/auth/lib/onboarding/view/view_qr_page.dart +++ b/auth/lib/onboarding/view/view_qr_page.dart @@ -1,4 +1,3 @@ - import 'dart:math'; import "package:ente_auth/l10n/l10n.dart"; @@ -10,7 +9,7 @@ import 'package:qr_flutter/qr_flutter.dart'; class ViewQrPage extends StatelessWidget { final Code? code; - ViewQrPage({this.code, Key? key}) : super(key: key); + ViewQrPage({this.code, super.key}); @override Widget build(BuildContext context) { @@ -22,15 +21,22 @@ class ViewQrPage extends StatelessWidget { appBar: AppBar( title: Text(l10n.qrCode), ), - body: SafeArea( - child: Center( + body: Center( + child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), child: Column( children: [ - QrImage( + QrImageView( data: code!.rawData, - foregroundColor: Theme.of(context).colorScheme.onBackground, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).colorScheme.onBackground, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).colorScheme.onBackground, + ), version: QrVersions.auto, size: qrSize, ), diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index 77d9b0437..ee0ad32bd 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -15,9 +15,8 @@ import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/store/offline_authenticator_db.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -75,10 +74,10 @@ class AuthenticatorService { final key = await getOrCreateAuthDataKey(mode); for (LocalAuthEntity e in result) { try { - final decryptedValue = await CryptoUtil.decryptChaCha( - Sodium.base642bin(e.encryptedData), + final decryptedValue = await CryptoUtil.decryptData( + CryptoUtil.base642bin(e.encryptedData), key, - Sodium.base642bin(e.header), + CryptoUtil.base642bin(e.header), ); final hasSynced = !(e.id == null || e.shouldSync); entities.add( @@ -101,12 +100,13 @@ class AuthenticatorService { AccountMode accountMode, ) async { var key = await getOrCreateAuthDataKey(accountMode); - final encryptedKeyData = await CryptoUtil.encryptChaCha( - utf8.encode(plainText) as Uint8List, + final encryptedKeyData = await CryptoUtil.encryptData( + utf8.encode(plainText), key, ); - String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); - String header = Sodium.bin2base64(encryptedKeyData.header!); + String encryptedData = + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!); + String header = CryptoUtil.bin2base64(encryptedKeyData.header!); final insertedID = accountMode.isOnline ? await _db.insert(encryptedData, header) : await _offlineDb.insert(encryptedData, header); @@ -123,12 +123,13 @@ class AuthenticatorService { AccountMode accountMode, ) async { var key = await getOrCreateAuthDataKey(accountMode); - final encryptedKeyData = await CryptoUtil.encryptChaCha( - utf8.encode(plainText) as Uint8List, + final encryptedKeyData = await CryptoUtil.encryptData( + utf8.encode(plainText), key, ); - String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); - String header = Sodium.bin2base64(encryptedKeyData.header!); + String encryptedData = + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!); + String header = CryptoUtil.bin2base64(encryptedKeyData.header!); final int affectedRows = accountMode.isOnline ? await _db.updateEntry(generatedID, encryptedData, header) : await _offlineDb.updateEntry(generatedID, encryptedData, header); @@ -191,25 +192,25 @@ class AuthenticatorService { Future _remoteToLocalSync() async { _logger.info('Initiating remote to local sync'); final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0; - _logger.info("Current sync is " + lastSyncTime.toString()); + _logger.info("Current sync is $lastSyncTime"); const int fetchLimit = 500; final List result = await _gateway.getDiff(lastSyncTime, limit: fetchLimit); - _logger.info(result.length.toString() + " entries fetched from remote"); + _logger.info("${result.length} entries fetched from remote"); if (result.isEmpty) { return; } final maxSyncTime = result.map((e) => e.updatedAt).reduce(max); List deletedIDs = result.where((element) => element.isDeleted).map((e) => e.id).toList(); - _logger.info(deletedIDs.length.toString() + " entries deleted"); + _logger.info("${deletedIDs.length} entries deleted"); result.removeWhere((element) => element.isDeleted); await _db.insertOrReplace(result); if (deletedIDs.isNotEmpty) { await _db.deleteByIDs(ids: deletedIDs); } await _prefs.setInt(_lastEntitySyncTime, maxSyncTime); - _logger.info("Setting synctime to " + maxSyncTime.toString()); + _logger.info("Setting synctime to $maxSyncTime"); if (result.length == fetchLimit) { _logger.info("Diff limit reached, pulling again"); await _remoteToLocalSync(); @@ -223,7 +224,7 @@ class AuthenticatorService { .where((element) => element.shouldSync || element.id == null) .toList(); _logger.info( - pendingUpdate.length.toString() + " entries to be updated at remote", + "${pendingUpdate.length} entries to be updated at remote", ); for (LocalAuthEntity entity in pendingUpdate) { if (entity.id == null) { @@ -262,21 +263,21 @@ class AuthenticatorService { try { final AuthKey response = await _gateway.getKey(); final authKey = CryptoUtil.decryptSync( - Sodium.base642bin(response.encryptedKey), + CryptoUtil.base642bin(response.encryptedKey), _config.getKey()!, - Sodium.base642bin(response.header), + CryptoUtil.base642bin(response.header), ); - await _config.setAuthSecretKey(Sodium.bin2base64(authKey)); + await _config.setAuthSecretKey(CryptoUtil.bin2base64(authKey)); return authKey; } on AuthenticatorKeyNotFound catch (e) { _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}"); final key = CryptoUtil.generateKey(); final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!); await _gateway.createKey( - Sodium.bin2base64(encryptedKeyData.encryptedData!), - Sodium.bin2base64(encryptedKeyData.nonce!), + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedKeyData.nonce!), ); - await _config.setAuthSecretKey(Sodium.bin2base64(key)); + await _config.setAuthSecretKey(CryptoUtil.bin2base64(key)); return key; } catch (e, s) { _logger.severe("Failed to getOrCreateAuthDataKey", e, s); diff --git a/auth/lib/services/billing_service.dart b/auth/lib/services/billing_service.dart index 75a4e5579..363425cd5 100644 --- a/auth/lib/services/billing_service.dart +++ b/auth/lib/services/billing_service.dart @@ -50,7 +50,7 @@ class BillingService { Future> _fetchPrivateBillingPlans() { return _dio.get( - _config.getHttpEndpoint() + "/billing/user-plans/", + "${_config.getHttpEndpoint()}/billing/user-plans/", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -60,7 +60,7 @@ class BillingService { } Future> _fetchPublicBillingPlans() { - return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2"); + return _dio.get("${_config.getHttpEndpoint()}/billing/plans/v2"); } Future verifySubscription( @@ -70,7 +70,7 @@ class BillingService { }) async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/verify-subscription", + "${_config.getHttpEndpoint()}/billing/verify-subscription", data: { "paymentProvider": paymentProvider ?? (Platform.isAndroid ? "playstore" : "appstore"), @@ -84,7 +84,7 @@ class BillingService { ), ); return Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { throw SubscriptionAlreadyClaimedError(); } else { @@ -100,7 +100,7 @@ class BillingService { if (_cachedSubscription == null) { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/billing/subscription", + "${_config.getHttpEndpoint()}/billing/subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -109,7 +109,7 @@ class BillingService { ); _cachedSubscription = Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -120,7 +120,7 @@ class BillingService { Future cancelStripeSubscription() async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/stripe/cancel-subscription", + "${_config.getHttpEndpoint()}/billing/stripe/cancel-subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -129,7 +129,7 @@ class BillingService { ); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -138,7 +138,7 @@ class BillingService { Future activateStripeSubscription() async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/stripe/activate-subscription", + "${_config.getHttpEndpoint()}/billing/stripe/activate-subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -147,7 +147,7 @@ class BillingService { ); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -158,7 +158,7 @@ class BillingService { }) async { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/billing/stripe/customer-portal", + "${_config.getHttpEndpoint()}/billing/stripe/customer-portal", queryParameters: { "redirectURL": kWebPaymentRedirectUrl, }, @@ -169,7 +169,7 @@ class BillingService { ), ); return response.data["url"]; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } diff --git a/auth/lib/services/local_authentication_service.dart b/auth/lib/services/local_authentication_service.dart index b7b000dd9..44c2a758a 100644 --- a/auth/lib/services/local_authentication_service.dart +++ b/auth/lib/services/local_authentication_service.dart @@ -1,15 +1,21 @@ +import 'dart:io'; + import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/utils/auth_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:logging/logging.dart'; class LocalAuthenticationService { LocalAuthenticationService._privateConstructor(); static final LocalAuthenticationService instance = LocalAuthenticationService._privateConstructor(); + final logger = Logger((LocalAuthenticationService).toString()); Future requestLocalAuthentication( BuildContext context, @@ -38,7 +44,7 @@ class LocalAuthenticationService { String errorDialogContent, [ String errorDialogTitle = "", ]) async { - if (await LocalAuthentication().isDeviceSupported()) { + if (await _isLocalAuthSupportedOnDevice()) { AppLock.of(context)!.disable(); final result = await requestAuthentication( context, @@ -65,6 +71,12 @@ class LocalAuthenticationService { } Future _isLocalAuthSupportedOnDevice() async { - return await LocalAuthentication().isDeviceSupported(); + try { + return Platform.isMacOS || Platform.isLinux + ? await FlutterLocalAuthentication().canAuthenticate() + : await LocalAuthentication().isDeviceSupported(); + } on MissingPluginException { + return false; + } } } diff --git a/auth/lib/services/notification_service.dart b/auth/lib/services/notification_service.dart index 60d12811b..63410eb7d 100644 --- a/auth/lib/services/notification_service.dart +++ b/auth/lib/services/notification_service.dart @@ -27,8 +27,7 @@ class NotificationService { _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); if (implementation != null) { - // ignore: unawaited_futures - implementation.requestPermission(); + await implementation.requestNotificationsPermission(); } } diff --git a/auth/lib/services/update_service.dart b/auth/lib/services/update_service.dart index 09f36df83..e102a5a94 100644 --- a/auth/lib/services/update_service.dart +++ b/auth/lib/services/update_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:ente_auth/core/constants.dart'; import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/services/notification_service.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -130,7 +131,8 @@ class UpdateService { bool isIndependent() { return flavor == "independent" || - _packageInfo.packageName.endsWith("independent"); + _packageInfo.packageName.endsWith("independent") || + PlatformUtil.isDesktop(); } } @@ -141,6 +143,7 @@ class LatestVersionInfo { final bool? shouldForceUpdate; final int lastSupportedVersionCode; final String? url; + final String? release; final int? size; final bool? shouldNotify; @@ -151,6 +154,7 @@ class LatestVersionInfo { this.shouldForceUpdate, this.lastSupportedVersionCode, this.url, + this.release, this.size, this.shouldNotify, ); @@ -163,6 +167,7 @@ class LatestVersionInfo { map['shouldForceUpdate'], map['lastSupportedVersionCode'] ?? 1, map['url'], + map['release'], map['size'], map['shouldNotify'], ); diff --git a/auth/lib/services/user_remote_flag_service.dart b/auth/lib/services/user_remote_flag_service.dart index 6961518aa..74deae584 100644 --- a/auth/lib/services/user_remote_flag_service.dart +++ b/auth/lib/services/user_remote_flag_service.dart @@ -96,7 +96,7 @@ class UserRemoteFlagService { queryParams["defaultValue"] = defaultValue; } final response = await _dio.get( - _config.getHttpEndpoint() + "/remote-store", + "${_config.getHttpEndpoint()}/remote-store", queryParameters: queryParams, options: Options( headers: { @@ -119,7 +119,7 @@ class UserRemoteFlagService { Future _updateKeyValue(String key, String value) async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/remote-store/update", + "${_config.getHttpEndpoint()}/remote-store/update", data: { "key": key, "value": value, diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index b870aa647..929468059 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -30,9 +30,9 @@ import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/passkey_page.dart'; import 'package:ente_auth/ui/two_factor_authentication_page.dart'; import 'package:ente_auth/ui/two_factor_recovery_page.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -80,7 +80,7 @@ class UserService { await dialog.show(); try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/ott", + "${_config.getHttpEndpoint()}/users/ott", data: {"email": email, "purpose": isChangeEmail ? "change" : ""}, ); await dialog.hide(); @@ -102,7 +102,7 @@ class UserService { return; } unawaited(showGenericErrorDialog(context: context)); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.info(e); if (e.response != null && e.response!.statusCode == 403) { @@ -129,7 +129,7 @@ class UserService { String type = "SubCancellation", }) async { await _dio.post( - _config.getHttpEndpoint() + "/anonymous/feedback", + "${_config.getHttpEndpoint()}/anonymous/feedback", data: {"feedback": feedback, "type": "type"}, ); } @@ -173,7 +173,7 @@ class UserService { try { final response = await _enteDio.get("/users/sessions"); return Sessions.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -187,7 +187,7 @@ class UserService { "token": token, }, ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -196,7 +196,7 @@ class UserService { Future leaveFamilyPlan() async { try { await _enteDio.delete("/family/leave"); - } on DioError catch (e) { + } on DioException catch (e) { _logger.warning('failed to leave family plan', e); rethrow; } @@ -306,11 +306,11 @@ class UserService { "ott": ott, }; if (!_config.isLoggedIn()) { - verifyData["source"] = 'auth:' + _getRefSource(); + verifyData["source"] = 'auth:${_getRefSource()}'; } try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/verify-email", + "${_config.getHttpEndpoint()}/users/verify-email", data: verifyData, ); await dialog.hide(); @@ -346,7 +346,7 @@ class UserService { // should never reach here throw Exception("unexpected response during email verification"); } - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); await dialog.hide(); if (e.response != null && e.response!.statusCode == 410) { @@ -410,7 +410,7 @@ class UserService { context.l10n.oops, context.l10n.verificationFailedPleaseTryAgain, ); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 403) { // ignore: unawaited_futures @@ -460,7 +460,7 @@ class UserService { Future getSrpAttributes(String email) async { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/users/srp/attributes", + "${_config.getHttpEndpoint()}/users/srp/attributes", queryParameters: { "email": email, }, @@ -470,7 +470,7 @@ class UserService { } else { throw Exception("get-srp-attributes action failed"); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 404) { throw SrpSetupNotCompleteError(); } @@ -523,7 +523,7 @@ class UserService { // ignore: need to calculate secret to get M1, unused_local_variable final clientS = client.calculateSecret(serverB); final clientM = client.calculateClientEvidenceMessage(); - // ignore: unused_local_variable + late Response _; if (setKeysRequest == null) { _ = await _enteDio.post( @@ -573,7 +573,7 @@ class UserService { late Uint8List keyEncryptionKey; _logger.finest('Start deriving key'); keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(userPassword) as Uint8List, + utf8.encode(userPassword), CryptoUtil.base642bin(srpAttributes.kekSalt), srpAttributes.memLimit, srpAttributes.opsLimit, @@ -596,7 +596,7 @@ class UserService { final A = client.generateClientCredentials(salt, identity, password); final createSessionResponse = await _dio.post( - _config.getHttpEndpoint() + "/users/srp/create-session", + "${_config.getHttpEndpoint()}/users/srp/create-session", data: { "srpUserID": srpAttributes.srpUserID, "srpA": base64Encode(SRP6Util.encodeBigInt(A!)), @@ -610,7 +610,7 @@ class UserService { final clientS = client.calculateSecret(serverB); final clientM = client.calculateClientEvidenceMessage(); final response = await _dio.post( - _config.getHttpEndpoint() + "/users/srp/verify-session", + "${_config.getHttpEndpoint()}/users/srp/verify-session", data: { "sessionID": sessionID, "srpUserID": srpAttributes.srpUserID, @@ -709,7 +709,7 @@ class UserService { await dialog.show(); try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/two-factor/verify", + "${_config.getHttpEndpoint()}/users/two-factor/verify", data: { "sessionID": sessionID, "code": code, @@ -729,7 +729,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -772,7 +772,7 @@ class UserService { await dialog.show(); try { final response = await _dio.get( - _config.getHttpEndpoint() + "/users/two-factor/recover", + "${_config.getHttpEndpoint()}/users/two-factor/recover", queryParameters: { "sessionID": sessionID, "twoFactorType": twoFactorTypeToString(type), @@ -794,7 +794,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -868,7 +868,7 @@ class UserService { } try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/two-factor/remove", + "${_config.getHttpEndpoint()}/users/two-factor/remove", data: { "sessionID": sessionID, "secret": secret, @@ -891,7 +891,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { diff --git a/auth/lib/services/window_listener_service.dart b/auth/lib/services/window_listener_service.dart new file mode 100644 index 000000000..9ceefef9a --- /dev/null +++ b/auth/lib/services/window_listener_service.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowListenerService { + late SharedPreferences _preferences; + + WindowListenerService._privateConstructor(); + + static final WindowListenerService instance = + WindowListenerService._privateConstructor(); + + Future init() async { + _preferences = await SharedPreferences.getInstance(); + } + + Size getWindowSize() { + final double windowWidth = _preferences.getDouble('windowWidth') ?? 450.0; + final double windowHeight = _preferences.getDouble('windowHeight') ?? 800.0; + return Size(windowWidth, windowHeight); + } + + Future onWindowResize() async { + // Save the window size to shared preferences + await _preferences.setDouble( + 'windowWidth', + (await windowManager.getSize()).width, + ); + await _preferences.setDouble( + 'windowHeight', + (await windowManager.getSize()).height, + ); + } +} diff --git a/auth/lib/store/authenticator_db.dart b/auth/lib/store/authenticator_db.dart index 0f05e83c0..deb57bc81 100644 --- a/auth/lib/store/authenticator_db.dart +++ b/auth/lib/store/authenticator_db.dart @@ -3,10 +3,12 @@ import 'dart:io'; import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/utils/directory_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class AuthenticatorDB { static const _databaseName = "ente.authenticator.db"; @@ -25,6 +27,16 @@ class AuthenticatorDB { } Future _initDatabase() async { + if (Platform.isWindows || Platform.isLinux) { + var databaseFactory = databaseFactoryFfi; + return await databaseFactory.openDatabase( + await DirectoryUtils.getDatabasePath(_databaseName), + options: OpenDatabaseOptions( + version: _databaseVersion, + onCreate: _onCreate, + ), + ); + } final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); @@ -166,7 +178,7 @@ class AuthenticatorDB { batch.delete(entityTable, where: whereID, whereArgs: [id]); } } - await batch.commit(); + final _ = await batch.commit(); debugPrint("Done"); } diff --git a/auth/lib/store/offline_authenticator_db.dart b/auth/lib/store/offline_authenticator_db.dart index 402d6590c..d1af51fff 100644 --- a/auth/lib/store/offline_authenticator_db.dart +++ b/auth/lib/store/offline_authenticator_db.dart @@ -3,10 +3,11 @@ import 'dart:io'; import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/utils/directory_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class OfflineAuthenticatorDB { static const _databaseName = "ente.offline_authenticator.db"; @@ -26,6 +27,16 @@ class OfflineAuthenticatorDB { } Future _initDatabase() async { + if (Platform.isWindows || Platform.isLinux) { + var databaseFactory = databaseFactoryFfi; + return await databaseFactory.openDatabase( + await DirectoryUtils.getDatabasePath(_databaseName), + options: OpenDatabaseOptions( + version: _databaseVersion, + onCreate: _onCreate, + ), + ); + } final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); @@ -152,7 +163,7 @@ class OfflineAuthenticatorDB { batch.delete(entityTable, where: whereID, whereArgs: [id]); } } - await batch.commit(); + final _ = await batch.commit(); debugPrint("Done"); } diff --git a/auth/lib/theme/colors.dart b/auth/lib/theme/colors.dart index 1d4a8517c..9ac9d2d7e 100644 --- a/auth/lib/theme/colors.dart +++ b/auth/lib/theme/colors.dart @@ -204,6 +204,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1); const Color _warning500 = Color.fromRGBO(255, 101, 101, 1); const Color _warning800 = Color(0xFFF53434); const Color warning500 = Color.fromRGBO(255, 101, 101, 1); +// ignore: unused_element const Color _warning400 = Color.fromRGBO(255, 111, 111, 1); const Color _caution500 = Color.fromRGBO(255, 194, 71, 1); diff --git a/auth/lib/ui/account/change_email_dialog.dart b/auth/lib/ui/account/change_email_dialog.dart index 7cfa10f4a..828970cd7 100644 --- a/auth/lib/ui/account/change_email_dialog.dart +++ b/auth/lib/ui/account/change_email_dialog.dart @@ -5,7 +5,7 @@ import 'package:ente_auth/utils/email_util.dart'; import 'package:flutter/material.dart'; class ChangeEmailDialog extends StatefulWidget { - const ChangeEmailDialog({Key? key}) : super(key: key); + const ChangeEmailDialog({super.key}); @override State createState() => _ChangeEmailDialogState(); diff --git a/auth/lib/ui/account/delete_account_page.dart b/auth/lib/ui/account/delete_account_page.dart index 056744974..b8779e8cb 100644 --- a/auth/lib/ui/account/delete_account_page.dart +++ b/auth/lib/ui/account/delete_account_page.dart @@ -7,15 +7,15 @@ import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/common/dialogs.dart'; import 'package:ente_auth/ui/common/gradient_button.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/email_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class DeleteAccountPage extends StatelessWidget { const DeleteAccountPage({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -150,6 +150,8 @@ class DeleteAccountPage extends StatelessWidget { l10n.initiateAccountDeleteTitle, ); + await PlatformUtil.refocusWindows(); + if (hasAuthenticated) { final choice = await showChoiceDialogOld( context, @@ -164,8 +166,10 @@ class DeleteAccountPage extends StatelessWidget { return; } final decryptChallenge = CryptoUtil.openSealSync( - Sodium.base642bin(response.encryptedChallenge), - Sodium.base642bin(Configuration.instance.getKeyAttributes()!.publicKey), + CryptoUtil.base642bin(response.encryptedChallenge), + CryptoUtil.base642bin( + Configuration.instance.getKeyAttributes()!.publicKey, + ), Configuration.instance.getSecretKey()!, ); final challengeResponseStr = utf8.decode(decryptChallenge); diff --git a/auth/lib/ui/account/email_entry_page.dart b/auth/lib/ui/account/email_entry_page.dart index a20f6e245..e728ecd8c 100644 --- a/auth/lib/ui/account/email_entry_page.dart +++ b/auth/lib/ui/account/email_entry_page.dart @@ -5,7 +5,7 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +14,7 @@ import 'package:step_progress_indicator/step_progress_indicator.dart'; import "package:styled_text/styled_text.dart"; class EmailEntryPage extends StatefulWidget { - const EmailEntryPage({Key? key}) : super(key: key); + const EmailEntryPage({super.key}); @override State createState() => _EmailEntryPageState(); @@ -190,6 +190,7 @@ class _EmailEntryPageState extends State { padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), child: TextFormField( keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, controller: _passwordController1, obscureText: !_password1Visible, enableSuggestions: true, @@ -427,15 +428,10 @@ class _EmailEntryPageState extends State { tags: { 'u-terms': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.termsOfServicesTitle, - "https://ente.io/terms", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.termsOfServicesTitle, + "https://ente.io/terms", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -443,15 +439,10 @@ class _EmailEntryPageState extends State { ), 'u-policy': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.privacyPolicyTitle, - "https://ente.io/privacy", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.privacyPolicyTitle, + "https://ente.io/privacy", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -494,15 +485,10 @@ class _EmailEntryPageState extends State { tags: { 'underline': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.encryption, - "https://ente.io/architecture", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.encryption, + "https://ente.io/architecture", ), style: const TextStyle( decoration: TextDecoration.underline, diff --git a/auth/lib/ui/account/login_page.dart b/auth/lib/ui/account/login_page.dart index df28f98a1..5b8aa0daa 100644 --- a/auth/lib/ui/account/login_page.dart +++ b/auth/lib/ui/account/login_page.dart @@ -6,13 +6,13 @@ import 'package:ente_auth/models/api/user/srp.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/account/login_pwd_verification_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:styled_text/styled_text.dart"; class LoginPage extends StatefulWidget { - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); @@ -25,6 +25,36 @@ class _LoginPageState extends State { Color? _emailInputFieldColor; final Logger _logger = Logger('_LoginPageState'); + Future onPressed() async { + await UserService.instance.setEmail(_email!); + Configuration.instance.resetVolatilePassword(); + SrpAttributes? attr; + bool isEmailVerificationEnabled = true; + try { + attr = await UserService.instance.getSrpAttributes(_email!); + isEmailVerificationEnabled = attr.isEmailMFAEnabled; + } catch (e) { + if (e is! SrpSetupNotCompleteError) { + _logger.severe('Error getting SRP attributes', e); + } + } + if (attr != null && !isEmailVerificationEnabled) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return LoginPasswordVerificationPage( + srpAttributes: attr!, + ); + }, + ), + ); + } else { + await UserService.instance + .sendOtt(context, _email!, isCreateAccountScreen: false); + } + FocusScope.of(context).unfocus(); + } + @override void initState() { _email = _config.getEmail(); @@ -60,36 +90,7 @@ class _LoginPageState extends State { isKeypadOpen: isKeypadOpen, isFormValid: _emailIsValid, buttonText: context.l10n.logInLabel, - onPressedFunction: () async { - await UserService.instance.setEmail(_email!); - Configuration.instance.resetVolatilePassword(); - SrpAttributes? attr; - bool isEmailVerificationEnabled = true; - try { - attr = await UserService.instance.getSrpAttributes(_email!); - isEmailVerificationEnabled = attr.isEmailMFAEnabled; - } catch (e) { - if (e is! SrpSetupNotCompleteError) { - _logger.severe('Error getting SRP attributes', e); - } - } - if (attr != null && !isEmailVerificationEnabled) { - // ignore: unawaited_futures - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return LoginPasswordVerificationPage( - srpAttributes: attr!, - ); - }, - ), - ); - } else { - await UserService.instance - .sendOtt(context, _email!, isCreateAccountScreen: false); - } - FocusScope.of(context).unfocus(); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -116,6 +117,8 @@ class _LoginPageState extends State { padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: TextFormField( autofillHints: const [AutofillHints.email], + onFieldSubmitted: + _emailIsValid ? (value) => onPressed() : null, decoration: InputDecoration( fillColor: _emailInputFieldColor, filled: true, @@ -179,15 +182,10 @@ class _LoginPageState extends State { tags: { 'u-terms': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.termsOfServicesTitle, - "https://ente.io/terms", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.termsOfServicesTitle, + "https://ente.io/terms", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -195,15 +193,10 @@ class _LoginPageState extends State { ), 'u-policy': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.privacyPolicyTitle, - "https://ente.io/privacy", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.privacyPolicyTitle, + "https://ente.io/privacy", ), style: const TextStyle( decoration: TextDecoration.underline, diff --git a/auth/lib/ui/account/login_pwd_verification_page.dart b/auth/lib/ui/account/login_pwd_verification_page.dart index 3bd39a1b7..2d2754ec1 100644 --- a/auth/lib/ui/account/login_pwd_verification_page.dart +++ b/auth/lib/ui/account/login_pwd_verification_page.dart @@ -1,6 +1,5 @@ import "package:dio/dio.dart"; import 'package:ente_auth/core/configuration.dart'; -import "package:ente_auth/core/errors.dart"; import "package:ente_auth/l10n/l10n.dart"; import "package:ente_auth/models/api/user/srp.dart"; import "package:ente_auth/services/user_service.dart"; @@ -9,6 +8,7 @@ import 'package:ente_auth/ui/common/dynamic_fab.dart'; import "package:ente_auth/ui/components/buttons/button_widget.dart"; import "package:ente_auth/utils/dialog_util.dart"; import "package:ente_auth/utils/email_util.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; @@ -19,8 +19,7 @@ import "package:logging/logging.dart"; // volatile password. class LoginPasswordVerificationPage extends StatefulWidget { final SrpAttributes srpAttributes; - const LoginPasswordVerificationPage({Key? key, required this.srpAttributes}) - : super(key: key); + const LoginPasswordVerificationPage({super.key, required this.srpAttributes}); @override State createState() => @@ -36,6 +35,11 @@ class _LoginPasswordVerificationPageState bool _passwordInFocus = false; bool _passwordVisible = false; + Future onPressed() async { + FocusScope.of(context).unfocus(); + await verifyPassword(context, _passwordController.text); + } + @override void initState() { super.initState(); @@ -77,10 +81,7 @@ class _LoginPasswordVerificationPageState isKeypadOpen: isKeypadOpen, isFormValid: _passwordController.text.isNotEmpty, buttonText: context.l10n.logInLabel, - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - await verifyPassword(context, _passwordController.text); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -101,7 +102,7 @@ class _LoginPasswordVerificationPageState password, dialog, ); - } on DioError catch (e, s) { + } on DioException catch (e, s) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 401) { _logger.severe('server reject, failed verify SRP login', e, s); @@ -112,7 +113,7 @@ class _LoginPasswordVerificationPageState ); } else { _logger.severe('API failure during SRP login', e, s); - if (e.type == DioErrorType.other) { + if (e.type == DioExceptionType.unknown) { await _showContactSupportDialog( context, context.l10n.noInternetConnection, @@ -229,6 +230,9 @@ class _LoginPasswordVerificationPageState Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: TextFormField( + onFieldSubmitted: _passwordController.text.isNotEmpty + ? (_) => onPressed() + : null, key: const ValueKey("passwordInputField"), autofillHints: const [AutofillHints.password], decoration: InputDecoration( diff --git a/auth/lib/ui/account/ott_verification_page.dart b/auth/lib/ui/account/ott_verification_page.dart index 8bcaf3e34..cc9661def 100644 --- a/auth/lib/ui/account/ott_verification_page.dart +++ b/auth/lib/ui/account/ott_verification_page.dart @@ -17,8 +17,8 @@ class OTTVerificationPage extends StatefulWidget { this.isChangeEmail = false, this.isCreateAccountScreen = false, this.isResetPasswordScreen = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _OTTVerificationPageState(); @@ -27,6 +27,23 @@ class OTTVerificationPage extends StatefulWidget { class _OTTVerificationPageState extends State { final _verificationCodeController = TextEditingController(); + Future onPressed() async { + if (widget.isChangeEmail) { + await UserService.instance.changeEmail( + context, + widget.email, + _verificationCodeController.text, + ); + } else { + await UserService.instance.verifyEmail( + context, + _verificationCodeController.text, + isResettingPasswordScreen: widget.isResetPasswordScreen, + ); + } + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -68,22 +85,9 @@ class _OTTVerificationPageState extends State { body: _getBody(), floatingActionButton: DynamicFAB( isKeypadOpen: isKeypadOpen, - isFormValid: !(_verificationCodeController.text.isEmpty), + isFormValid: _verificationCodeController.text.isNotEmpty, buttonText: l10n.verify, - onPressedFunction: () { - if (widget.isChangeEmail) { - UserService.instance.changeEmail( - context, - widget.email, - _verificationCodeController.text, - ); - } else { - UserService.instance - .verifyEmail(context, _verificationCodeController.text, - isResettingPasswordScreen: widget.isResetPasswordScreen,); - } - FocusScope.of(context).unfocus(); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -160,6 +164,9 @@ class _OTTVerificationPageState extends State { padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), child: TextFormField( style: Theme.of(context).textTheme.titleMedium, + onFieldSubmitted: _verificationCodeController.text.isNotEmpty + ? (_) => onPressed() + : null, decoration: InputDecoration( filled: true, hintText: l10n.tapToEnterCode, diff --git a/auth/lib/ui/account/password_entry_page.dart b/auth/lib/ui/account/password_entry_page.dart index 7411d34a8..9b1ce6181 100644 --- a/auth/lib/ui/account/password_entry_page.dart +++ b/auth/lib/ui/account/password_entry_page.dart @@ -4,11 +4,11 @@ import 'package:ente_auth/models/key_gen_result.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/account/recovery_key_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,7 +25,7 @@ enum PasswordEntryMode { class PasswordEntryPage extends StatefulWidget { final PasswordEntryMode mode; - const PasswordEntryPage({required this.mode, Key? key}) : super(key: key); + const PasswordEntryPage({required this.mode, super.key}); @override State createState() => _PasswordEntryPageState(); @@ -149,227 +149,239 @@ class _PasswordEntryPageState extends State { children: [ Expanded( child: AutofillGroup( - child: ListView( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - buttonTextAndHeading, - style: Theme.of(context).textTheme.headlineMedium, + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 30, + horizontal: 20, + ), + child: Text( + buttonTextAndHeading, + style: Theme.of(context).textTheme.headlineMedium, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - widget.mode == PasswordEntryMode.set - ? context.l10n.enterPasswordToEncrypt - : context.l10n.enterNewPasswordToEncrypt, - textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + widget.mode == PasswordEntryMode.set + ? context.l10n.enterPasswordToEncrypt + : context.l10n.enterNewPasswordToEncrypt, + textAlign: TextAlign.start, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + ), ), - ), - const Padding(padding: EdgeInsets.all(8)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: StyledText( - text: context.l10n.passwordWarning, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - tags: { - 'underline': StyledTextTag( - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, + const Padding(padding: EdgeInsets.all(8)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: StyledText( + text: context.l10n.passwordWarning, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + tags: { + 'underline': StyledTextTag( + style: + Theme.of(context).textTheme.titleMedium!.copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + }, + ), + ), + const Padding(padding: EdgeInsets.all(12)), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + autofillHints: const [AutofillHints.newPassword], + onFieldSubmitted: (_) { + do { + FocusScope.of(context).nextFocus(); + } while (FocusScope.of(context).focusedChild!.context == + null); + }, + decoration: InputDecoration( + fillColor: + _isPasswordValid ? _validFieldValueColor : null, + filled: true, + hintText: context.l10n.password, + contentPadding: const EdgeInsets.all(20), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + suffixIcon: _password1InFocus + ? IconButton( + icon: Icon( + _password1Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, ), + onPressed: () { + setState(() { + _password1Visible = !_password1Visible; + }); + }, + ) + : _isPasswordValid + ? Icon( + Icons.check, + color: Theme.of(context) + .inputDecorationTheme + .focusedBorder! + .borderSide + .color, + ) + : null, ), - }, - ), - ), - const Padding(padding: EdgeInsets.all(12)), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( - fillColor: - _isPasswordValid ? _validFieldValueColor : null, - filled: true, - hintText: context.l10n.password, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _password1InFocus - ? IconButton( - icon: Icon( - _password1Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password1Visible = !_password1Visible; - }); - }, - ) - : _isPasswordValid - ? Icon( - Icons.check, - color: Theme.of(context) - .inputDecorationTheme - .focusedBorder! - .borderSide - .color, - ) - : null, - ), - obscureText: !_password1Visible, - controller: _passwordController1, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.visiblePassword, - onChanged: (password) { - setState(() { - _passwordInInputBox = password; - _passwordStrength = estimatePasswordStrength(password); - _isPasswordValid = - _passwordStrength >= kMildPasswordStrengthThreshold; - _passwordsMatch = _passwordInInputBox == - _passwordInInputConfirmationBox; - }); - }, - textInputAction: TextInputAction.next, - focusNode: _password1FocusNode, - ), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - keyboardType: TextInputType.visiblePassword, - controller: _passwordController2, - obscureText: !_password2Visible, - autofillHints: const [AutofillHints.newPassword], - onEditingComplete: () => TextInput.finishAutofillContext(), - decoration: InputDecoration( - fillColor: _passwordsMatch ? _validFieldValueColor : null, - filled: true, - hintText: context.l10n.confirmPassword, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, - ), - suffixIcon: _password2InFocus - ? IconButton( - icon: Icon( - _password2Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password2Visible = !_password2Visible; - }); - }, - ) - : _passwordsMatch - ? Icon( - Icons.check, - color: Theme.of(context) - .inputDecorationTheme - .focusedBorder! - .borderSide - .color, - ) - : null, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), - focusNode: _password2FocusNode, - onChanged: (cnfPassword) { - setState(() { - _passwordInInputConfirmationBox = cnfPassword; - if (_passwordInInputBox != '') { + obscureText: !_password1Visible, + controller: _passwordController1, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + onChanged: (password) { + setState(() { + _passwordInInputBox = password; + _passwordStrength = + estimatePasswordStrength(password); + _isPasswordValid = _passwordStrength >= + kMildPasswordStrengthThreshold; _passwordsMatch = _passwordInInputBox == _passwordInInputConfirmationBox; - } - }); - }, - ), - ), - Opacity( - opacity: - (_passwordInInputBox != '') && _password1InFocus ? 1 : 0, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text( - context.l10n.passwordStrength(passwordStrengthText), - style: TextStyle( - color: passwordStrengthColor, - ), + }); + }, + textInputAction: TextInputAction.next, + focusNode: _password1FocusNode, ), ), - ), - const SizedBox(height: 8), - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.howItWorks, - "https://ente.io/architecture", - ); - }, - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: RichText( - text: TextSpan( - text: context.l10n.howItWorks, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + keyboardType: TextInputType.visiblePassword, + controller: _passwordController2, + obscureText: !_password2Visible, + autofillHints: const [AutofillHints.newPassword], + onEditingComplete: () => + TextInput.finishAutofillContext(), + decoration: InputDecoration( + fillColor: + _passwordsMatch ? _validFieldValueColor : null, + filled: true, + hintText: context.l10n.confirmPassword, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + suffixIcon: _password2InFocus + ? IconButton( + icon: Icon( + _password2Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, ), + onPressed: () { + setState(() { + _password2Visible = !_password2Visible; + }); + }, + ) + : _passwordsMatch + ? Icon( + Icons.check, + color: Theme.of(context) + .inputDecorationTheme + .focusedBorder! + .borderSide + .color, + ) + : null, + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + ), + focusNode: _password2FocusNode, + onChanged: (cnfPassword) { + setState(() { + _passwordInInputConfirmationBox = cnfPassword; + if (_passwordInInputBox != '') { + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + } + }); + }, + ), + ), + Opacity( + opacity: (_passwordInInputBox != '') && _password1InFocus + ? 1 + : 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + child: Text( + context.l10n.passwordStrength(passwordStrengthText), + style: TextStyle( + color: passwordStrengthColor, + ), ), ), ), - ), - const Padding(padding: EdgeInsets.all(20)), - ], + const SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + PlatformUtil.openWebView( + context, + context.l10n.howItWorks, + "https://ente.io/architecture", + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: RichText( + text: TextSpan( + text: context.l10n.howItWorks, + style: + Theme.of(context).textTheme.titleMedium!.copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ), + ), + ), + const Padding(padding: EdgeInsets.all(20)), + ], + ), ), ), ), @@ -463,6 +475,7 @@ class _PasswordEntryPageState extends State { showGenericErrorDialog(context: context); } } + // ignore: unawaited_futures routeToPage( context, diff --git a/auth/lib/ui/account/password_reentry_page.dart b/auth/lib/ui/account/password_reentry_page.dart index f729f935e..261f41db5 100644 --- a/auth/lib/ui/account/password_reentry_page.dart +++ b/auth/lib/ui/account/password_reentry_page.dart @@ -9,14 +9,14 @@ import 'package:ente_auth/ui/account/recovery_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/home_page.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/email_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class PasswordReentryPage extends StatefulWidget { - const PasswordReentryPage({Key? key}) : super(key: key); + const PasswordReentryPage({super.key}); @override State createState() => _PasswordReentryPageState(); @@ -261,8 +261,8 @@ class _PasswordReentryPageState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Wrap( - alignment: WrapAlignment.spaceBetween, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( behavior: HitTestBehavior.opaque, @@ -275,13 +275,17 @@ class _PasswordReentryPageState extends State { ), ); }, - child: Text( - context.l10n.forgotPassword, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + child: Center( + child: Text( + context.l10n.forgotPassword, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), ), ), GestureDetector( @@ -297,13 +301,17 @@ class _PasswordReentryPageState extends State { Navigator.of(context) .popUntil((route) => route.isFirst); }, - child: Text( - context.l10n.changeEmail, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + child: Center( + child: Text( + context.l10n.changeEmail, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), ), ), ], diff --git a/auth/lib/ui/account/recovery_key_page.dart b/auth/lib/ui/account/recovery_key_page.dart index c5ca66fdc..a74b2daec 100644 --- a/auth/lib/ui/account/recovery_key_page.dart +++ b/auth/lib/ui/account/recovery_key_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' as io; import 'package:bip39/bip39.dart' as bip39; @@ -7,7 +8,10 @@ import 'package:ente_auth/core/constants.dart'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/common/gradient_button.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; @@ -27,7 +31,7 @@ class RecoveryKeyPage extends StatefulWidget { const RecoveryKeyPage( this.recoveryKey, this.doneText, { - Key? key, + super.key, this.showAppBar, this.onDone, this.isDismissible, @@ -35,7 +39,7 @@ class RecoveryKeyPage extends StatefulWidget { this.text, this.subText, this.showProgressBar = false, - }) : super(key: key); + }); @override State createState() => _RecoveryKeyPageState(); @@ -44,7 +48,7 @@ class RecoveryKeyPage extends StatefulWidget { class _RecoveryKeyPageState extends State { bool _hasTriedToSave = false; final _recoveryKeyFile = io.File( - Configuration.instance.getTempDirectory() + "ente-recovery-key.txt", + "${Configuration.instance.getTempDirectory()}ente-recovery-key.txt", ); @override @@ -61,6 +65,21 @@ class _RecoveryKeyPageState extends State { ? 32 : 120; + Future copy() async { + await Clipboard.setData( + ClipboardData( + text: recoveryKey, + ), + ); + showShortToast( + context, + context.l10n.recoveryKeyCopiedToClipboard, + ); + setState(() { + _hasTriedToSave = true; + }); + } + return Scaffold( appBar: widget.showProgressBar ? AppBar( @@ -113,62 +132,73 @@ class _RecoveryKeyPageState extends State { style: Theme.of(context).textTheme.titleMedium, ), const Padding(padding: EdgeInsets.only(top: 24)), - DottedBorder( - color: const Color.fromARGB(255, 105, 17, 127), - //color of dotted/dash line - strokeWidth: 1, - //thickness of dash/dots - dashPattern: const [6, 6], - radius: const Radius.circular(8), - //dash patterns, 10 is dash width, 6 is space width - child: SizedBox( - //inner container - // height: 120, //height of inner container - width: double - .infinity, //width to 100% match to parent container. - // ignore: prefer_const_literals_to_create_immutables - child: Column( - children: [ - GestureDetector( - onTap: () async { - await Clipboard.setData( - ClipboardData(text: recoveryKey), - ); - showShortToast( - context, - context.l10n.recoveryKeyCopiedToClipboard, - ); - setState(() { - _hasTriedToSave = true; - }); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: const Color.fromRGBO( - 49, - 155, - 86, - .2, - ), + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x8E9610D6), + Color(0x8E9F4FC6), + ], + stops: [0.0, 0.9753], + ), + ), + child: DottedBorder( + padding: EdgeInsets.zero, + borderType: BorderType.RRect, + strokeWidth: 1, + color: const Color(0xFF6B6B6B), + dashPattern: const [6, 6], + radius: const Radius.circular(8), + child: SizedBox( + width: double.infinity, + child: Stack( + children: [ + Column( + children: [ + Builder( + builder: (context) { + final content = Container( + padding: const EdgeInsets.all(20), + width: double.infinity, + child: Text( + recoveryKey, + textAlign: TextAlign.justify, + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ); + + if (PlatformUtil.isMobile()) { + return GestureDetector( + onTap: () async => await copy(), + child: content, + ); + } else { + return SelectableRegion( + focusNode: FocusNode(), + selectionControls: + PlatformUtil.selectionControls, + child: content, + ); + } + }, ), - borderRadius: const BorderRadius.all( - Radius.circular(2), - ), - color: Theme.of(context) - .colorScheme - .recoveryKeyBoxColor, - ), - padding: const EdgeInsets.all(20), - width: double.infinity, - child: Text( - recoveryKey, - style: - Theme.of(context).textTheme.bodyLarge, + ], + ), + Positioned( + right: 0, + top: 0, + child: PlatformCopy( + onPressed: copy, ), ), - ), - ], + ], + ), ), ), ), @@ -193,7 +223,7 @@ class _RecoveryKeyPageState extends State { ), ), ], - ), // columnEnds + ), ), ), ); @@ -207,12 +237,15 @@ class _RecoveryKeyPageState extends State { final List childrens = []; if (!_hasTriedToSave) { childrens.add( - ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: () async { - await _saveKeys(); - }, - child: Text(context.l10n.doThisLater), + SizedBox( + height: 56, + child: ElevatedButton( + style: Theme.of(context).colorScheme.optionalActionButtonStyle, + onPressed: () async { + await _saveKeys(); + }, + child: Text(context.l10n.doThisLater), + ), ), ); childrens.add(const SizedBox(height: 10)); @@ -221,19 +254,32 @@ class _RecoveryKeyPageState extends State { childrens.add( GradientButton( onTap: () async { - await _shareRecoveryKey(recoveryKey); + await shareDialog( + context, + context.l10n.recoveryKey, + saveAction: () async { + await _saveRecoveryKey(recoveryKey); + }, + sendAction: () async { + await _shareRecoveryKey(recoveryKey); + }, + ); }, text: context.l10n.saveKey, ), ); + if (_hasTriedToSave) { childrens.add(const SizedBox(height: 10)); childrens.add( - ElevatedButton( - child: Text(widget.doneText), - onPressed: () async { - await _saveKeys(); - }, + SizedBox( + height: 56, + child: ElevatedButton( + child: Text(widget.doneText), + onPressed: () async { + await _saveKeys(); + }, + ), ), ); } @@ -241,11 +287,34 @@ class _RecoveryKeyPageState extends State { return childrens; } + Future _saveRecoveryKey(String recoveryKey) async { + final bytes = utf8.encode(recoveryKey); + final time = DateTime.now().millisecondsSinceEpoch; + + await PlatformUtil.shareFile( + "ente_recovery_key_$time", + "txt", + bytes, + MimeType.text, + ); + + if (mounted) { + showToast( + context, + context.l10n.recoveryKeySaved, + ); + setState(() { + _hasTriedToSave = true; + }); + } + } + Future _shareRecoveryKey(String recoveryKey) async { if (_recoveryKeyFile.existsSync()) { await _recoveryKeyFile.delete(); } _recoveryKeyFile.writeAsStringSync(recoveryKey); + // ignore: deprecated_member_use await Share.shareFiles([_recoveryKeyFile.path]); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { @@ -264,3 +333,24 @@ class _RecoveryKeyPageState extends State { widget.onDone!(); } } + +class PlatformCopy extends StatelessWidget { + const PlatformCopy({ + super.key, + required this.onPressed, + }); + + final void Function() onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () => onPressed(), + visualDensity: VisualDensity.compact, + icon: const Icon( + Icons.copy, + size: 16, + ), + ); + } +} diff --git a/auth/lib/ui/account/recovery_page.dart b/auth/lib/ui/account/recovery_page.dart index f0d5199d1..137b8ce43 100644 --- a/auth/lib/ui/account/recovery_page.dart +++ b/auth/lib/ui/account/recovery_page.dart @@ -1,8 +1,5 @@ - - -import 'dart:ui'; - import 'package:ente_auth/core/configuration.dart'; +import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/account/password_entry_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; import 'package:ente_auth/utils/dialog_util.dart'; @@ -10,7 +7,7 @@ import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; class RecoveryPage extends StatefulWidget { - const RecoveryPage({Key? key}) : super(key: key); + const RecoveryPage({super.key}); @override State createState() => _RecoveryPageState(); @@ -19,6 +16,36 @@ class RecoveryPage extends StatefulWidget { class _RecoveryPageState extends State { final _recoveryKey = TextEditingController(); + Future onPressed() async { + FocusScope.of(context).unfocus(); + final dialog = createProgressDialog(context, "Decrypting..."); + await dialog.show(); + try { + await Configuration.instance.recover(_recoveryKey.text.trim()); + await dialog.hide(); + showToast(context, "Recovery successful!"); + await Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (BuildContext context) { + return const PopScope( + canPop: false, + child: PasswordEntryPage( + mode: PasswordEntryMode.reset, + ), + ); + }, + ), + ); + } catch (e) { + await dialog.hide(); + String errMessage = 'The recovery key you entered is incorrect'; + if (e is AssertionError) { + errMessage = '$errMessage : ${e.message}'; + } + await showErrorDialog(context, "Incorrect recovery key", errMessage); + } + } + @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; @@ -46,37 +73,7 @@ class _RecoveryPageState extends State { isKeypadOpen: isKeypadOpen, isFormValid: _recoveryKey.text.isNotEmpty, buttonText: 'Recover', - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog(context, "Decrypting..."); - await dialog.show(); - try { - await Configuration.instance.recover(_recoveryKey.text.trim()); - await dialog.hide(); - showToast(context, "Recovery successful!"); - // ignore: unawaited_futures - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async => false, - child: const PasswordEntryPage( - mode: PasswordEntryMode.reset, - ), - ); - }, - ), - ); - } catch (e) { - await dialog.hide(); - String errMessage = 'The recovery key you entered is incorrect'; - if (e is AssertionError) { - errMessage = '$errMessage : ${e.message}'; - } - // ignore: unawaited_futures - showErrorDialog(context, "Incorrect recovery key", errMessage); - } - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -89,7 +86,7 @@ class _RecoveryPageState extends State { padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), child: Text( - 'Forgot password', + context.l10n.forgotPassword, style: Theme.of(context).textTheme.headlineMedium, ), ), @@ -140,12 +137,14 @@ class _RecoveryPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Center( child: Text( - "No recovery key?", - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + context.l10n.noRecoveryKeyTitle, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), ), ), ), diff --git a/auth/lib/ui/account/request_pwd_verification_page.dart b/auth/lib/ui/account/request_pwd_verification_page.dart index d852859fa..5901d3bd4 100644 --- a/auth/lib/ui/account/request_pwd_verification_page.dart +++ b/auth/lib/ui/account/request_pwd_verification_page.dart @@ -5,10 +5,9 @@ import 'package:ente_auth/core/configuration.dart'; import "package:ente_auth/l10n/l10n.dart"; import "package:ente_auth/theme/ente_theme.dart"; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import "package:ente_auth/utils/crypto_util.dart"; import "package:ente_auth/utils/dialog_util.dart"; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import "package:flutter_sodium/flutter_sodium.dart"; import "package:logging/logging.dart"; typedef OnPasswordVerifiedFn = Future Function(Uint8List bytes); @@ -17,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget { final OnPasswordVerifiedFn onPasswordVerified; final Function? onPasswordError; - const RequestPasswordVerificationPage( - {super.key, required this.onPasswordVerified, this.onPasswordError,}); + const RequestPasswordVerificationPage({ + super.key, + required this.onPasswordVerified, + this.onPasswordError, + }); @override State createState() => @@ -82,15 +84,15 @@ class _RequestPasswordVerificationPageState try { final attributes = Configuration.instance.getKeyAttributes()!; final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(_passwordController.text) as Uint8List, - Sodium.base642bin(attributes.kekSalt), + utf8.encode(_passwordController.text), + CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit, attributes.opsLimit, ); CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedKey), + CryptoUtil.base642bin(attributes.encryptedKey), keyEncryptionKey, - Sodium.base642bin(attributes.keyDecryptionNonce), + CryptoUtil.base642bin(attributes.keyDecryptionNonce), ); await dialog.show(); // pop diff --git a/auth/lib/ui/account/sessions_page.dart b/auth/lib/ui/account/sessions_page.dart index 31b7e7a41..1815b20e2 100644 --- a/auth/lib/ui/account/sessions_page.dart +++ b/auth/lib/ui/account/sessions_page.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class SessionsPage extends StatefulWidget { - const SessionsPage({Key? key}) : super(key: key); + const SessionsPage({super.key}); @override State createState() => _SessionsPageState(); diff --git a/auth/lib/ui/account/verify_recovery_page.dart b/auth/lib/ui/account/verify_recovery_page.dart index 2633afe33..03ed81fdf 100644 --- a/auth/lib/ui/account/verify_recovery_page.dart +++ b/auth/lib/ui/account/verify_recovery_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:bip39/bip39.dart' as bip39; import 'package:dio/dio.dart'; import 'package:ente_auth/core/configuration.dart'; @@ -12,12 +10,13 @@ import 'package:ente_auth/ui/common/gradient_button.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; class VerifyRecoveryPage extends StatefulWidget { - const VerifyRecoveryPage({Key? key}) : super(key: key); + const VerifyRecoveryPage({super.key}); @override State createState() => _VerifyRecoveryPageState(); @@ -34,14 +33,14 @@ class _VerifyRecoveryPageState extends State { try { final String inputKey = _recoveryKey.text.trim(); final String recoveryKey = - Sodium.bin2hex(Configuration.instance.getRecoveryKey()); + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey); if (inputKey == recoveryKey || inputKey == recoveryKeyWords) { try { await UserRemoteFlagService.instance.markRecoveryVerificationAsDone(); } catch (e) { await dialog.hide(); - if (e is DioError && e.type == DioErrorType.other) { + if (e is DioException && e.type == DioExceptionType.unknown) { await showErrorDialog( context, "No internet connection", @@ -88,12 +87,14 @@ class _VerifyRecoveryPageState extends State { context, "Please authenticate to view your recovery key", ); + await PlatformUtil.refocusWindows(); + if (hasAuthenticated) { String recoveryKey; try { - recoveryKey = Sodium.bin2hex(Configuration.instance.getRecoveryKey()); - // ignore: unawaited_futures - routeToPage( + recoveryKey = + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); + await routeToPage( context, RecoveryKeyPage( recoveryKey, diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index 1dbb4358e..b524a0c23 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -6,12 +6,12 @@ class CodeTimerProgress extends StatefulWidget { final int period; CodeTimerProgress({ - Key? key, + super.key, required this.period, - }) : super(key: key); + }); @override - _CodeTimerProgressState createState() => _CodeTimerProgressState(); + State createState() => _CodeTimerProgressState(); } class _CodeTimerProgressState extends State diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 7e0f78be3..f97e865ec 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -14,9 +14,11 @@ import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/code_timer_progress.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:ente_auth/utils/totp_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; @@ -24,7 +26,7 @@ import 'package:move_to_background/move_to_background.dart'; class CodeWidget extends StatefulWidget { final Code code; - const CodeWidget(this.code, {Key? key}) : super(key: key); + const CodeWidget(this.code, {super.key}); @override State createState() => _CodeWidgetState(); @@ -84,83 +86,121 @@ class _CodeWidgetState extends State { final l10n = context.l10n; return Container( margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8), - child: Slidable( - key: ValueKey(widget.code.hashCode), - endActionPane: ActionPane( - extentRatio: 0.60, - motion: const ScrollMotion(), - children: [ - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onShowQrPressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: - Theme.of(context).colorScheme.inverseBackgroundColor, - icon: Icons.qr_code_2_outlined, - label: "QR", - padding: const EdgeInsets.only(left: 4, right: 0), - spacing: 8, - ), - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onEditPressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: - Theme.of(context).colorScheme.inverseBackgroundColor, - icon: Icons.edit_outlined, - label: l10n.edit, - padding: const EdgeInsets.only(left: 4, right: 0), - spacing: 8, - ), - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onDeletePressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: const Color(0xFFFE4A49), - icon: Icons.delete, - label: l10n.delete, - padding: const EdgeInsets.only(left: 0, right: 0), - spacing: 8, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - color: Theme.of(context).colorScheme.codeCardBackgroundColor, - child: Material( - color: Colors.transparent, - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - onTap: () { - _copyCurrentOTPToClipboard(); - }, - onDoubleTap: isMaskingEnabled - ? () { - setState( - () { - _hideCode = !_hideCode; - }, - ); - } - : null, - onLongPress: () { - _copyCurrentOTPToClipboard(); - }, - child: _getCardContents(l10n), + child: Builder( + builder: (context) { + if (PlatformUtil.isDesktop()) { + return ContextMenuRegion( + contextMenu: ContextMenu( + entries: [ + MenuItem( + label: 'QR', + icon: Icons.qr_code_2_outlined, + onSelected: () => _onShowQrPressed(null), + ), + MenuItem( + label: l10n.edit, + icon: Icons.edit, + onSelected: () => _onEditPressed(null), + ), + const MenuDivider(), + MenuItem( + label: l10n.delete, + value: "Delete", + icon: Icons.delete, + onSelected: () => _onDeletePressed(null), + ), + ], + padding: const EdgeInsets.all(8.0), ), + child: _clippedCard(l10n), + ); + } + + return Slidable( + key: ValueKey(widget.code.hashCode), + endActionPane: ActionPane( + extentRatio: 0.60, + motion: const ScrollMotion(), + children: [ + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onShowQrPressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: + Theme.of(context).colorScheme.inverseBackgroundColor, + icon: Icons.qr_code_2_outlined, + label: "QR", + padding: const EdgeInsets.only(left: 4, right: 0), + spacing: 8, + ), + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onEditPressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: + Theme.of(context).colorScheme.inverseBackgroundColor, + icon: Icons.edit_outlined, + label: l10n.edit, + padding: const EdgeInsets.only(left: 4, right: 0), + spacing: 8, + ), + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onDeletePressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: const Color(0xFFFE4A49), + icon: Icons.delete, + label: l10n.delete, + padding: const EdgeInsets.only(left: 0, right: 0), + spacing: 8, + ), + ], ), + child: Builder( + builder: (context) => _clippedCard(l10n), + ), + ); + }, + ), + ); + } + + Widget _clippedCard(AppLocalizations l10n) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: Theme.of(context).colorScheme.codeCardBackgroundColor, + child: Material( + color: Colors.transparent, + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + onTap: () { + _copyCurrentOTPToClipboard(); + }, + onDoubleTap: isMaskingEnabled + ? () { + setState( + () { + _hideCode = !_hideCode; + }, + ); + } + : null, + onLongPress: () { + _copyCurrentOTPToClipboard(); + }, + child: _getCardContents(l10n), ), ), ), @@ -373,9 +413,10 @@ class _CodeWidgetState extends State { } Future _onEditPressed(_) async { - bool _isAuthSuccessful = await LocalAuthenticationService.instance + bool isAuthSuccessful = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.editCodeAuthMessage); - if (!_isAuthSuccessful) { + await PlatformUtil.refocusWindows(); + if (!isAuthSuccessful) { return; } final Code? code = await Navigator.of(context).push( @@ -391,9 +432,10 @@ class _CodeWidgetState extends State { } Future _onShowQrPressed(_) async { - bool _isAuthSuccessful = await LocalAuthenticationService.instance + bool isAuthSuccessful = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.showQRAuthMessage); - if (!_isAuthSuccessful) { + await PlatformUtil.refocusWindows(); + if (!isAuthSuccessful) { return; } // ignore: unused_local_variable @@ -407,14 +449,15 @@ class _CodeWidgetState extends State { } void _onDeletePressed(_) async { - bool _isAuthSuccessful = + bool isAuthSuccessful = await LocalAuthenticationService.instance.requestLocalAuthentication( context, context.l10n.deleteCodeAuthMessage, ); - if (!_isAuthSuccessful) { + if (!isAuthSuccessful) { return; } + FocusScope.of(context).requestFocus(); final l10n = context.l10n; await showChoiceActionSheet( context, @@ -451,7 +494,7 @@ class _CodeWidgetState extends State { code = code.replaceAll(RegExp(r'\d'), '•'); } if (code.length == 6) { - return code.substring(0, 3) + " " + code.substring(3, 6); + return "${code.substring(0, 3)} ${code.substring(3, 6)}"; } return code; } diff --git a/auth/lib/ui/common/bottom_shadow.dart b/auth/lib/ui/common/bottom_shadow.dart index 08a73564f..a57e5232a 100644 --- a/auth/lib/ui/common/bottom_shadow.dart +++ b/auth/lib/ui/common/bottom_shadow.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; class BottomShadowWidget extends StatelessWidget { final double offsetDy; final Color? shadowColor; - const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key? key}) - : super(key: key); + const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, super.key}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/DividerWithPadding.dart b/auth/lib/ui/common/divider_with_padding.dart similarity index 92% rename from auth/lib/ui/common/DividerWithPadding.dart rename to auth/lib/ui/common/divider_with_padding.dart index 94b20d9c2..0470bdf34 100644 --- a/auth/lib/ui/common/DividerWithPadding.dart +++ b/auth/lib/ui/common/divider_with_padding.dart @@ -1,17 +1,15 @@ - - import 'package:flutter/material.dart'; class DividerWithPadding extends StatelessWidget { final double left, top, right, bottom, thinckness; const DividerWithPadding({ - Key? key, + super.key, this.left = 0, this.top = 0, this.right = 0, this.bottom = 0, this.thinckness = 0.5, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/dynamic_fab.dart b/auth/lib/ui/common/dynamic_fab.dart index 0f9771627..8ccd9b855 100644 --- a/auth/lib/ui/common/dynamic_fab.dart +++ b/auth/lib/ui/common/dynamic_fab.dart @@ -10,12 +10,12 @@ class DynamicFAB extends StatelessWidget { final Function? onPressedFunction; const DynamicFAB({ - Key? key, + super.key, this.isKeypadOpen, this.buttonText, this.isFormValid, this.onPressedFunction, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -60,6 +60,7 @@ class DynamicFAB extends StatelessWidget { } else { return Container( width: double.infinity, + height: 56, padding: const EdgeInsets.symmetric(horizontal: 20), child: OutlinedButton( onPressed: diff --git a/auth/lib/ui/common/gradient_button.dart b/auth/lib/ui/common/gradient_button.dart index 5b021f613..8a24c6832 100644 --- a/auth/lib/ui/common/gradient_button.dart +++ b/auth/lib/ui/common/gradient_button.dart @@ -1,5 +1,3 @@ - - import 'package:flutter/material.dart'; class GradientButton extends StatelessWidget { @@ -15,17 +13,21 @@ class GradientButton extends StatelessWidget { // padding between the text and icon final double paddingValue; + // used when two icons are in row + final bool reversedGradient; + const GradientButton({ - Key? key, + super.key, this.linearGradientColors = const [ Color.fromARGB(255, 133, 44, 210), Color.fromARGB(255, 187, 26, 93), ], + this.reversedGradient = false, this.onTap, this.text = '', this.iconData, this.paddingValue = 0.0, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -71,7 +73,9 @@ class GradientButton extends StatelessWidget { gradient: LinearGradient( begin: const Alignment(0.1, -0.9), end: const Alignment(-0.6, 0.9), - colors: linearGradientColors, + colors: reversedGradient + ? linearGradientColors.reversed.toList() + : linearGradientColors, ), borderRadius: BorderRadius.circular(8), ), diff --git a/auth/lib/ui/common/linear_progress_dialog.dart b/auth/lib/ui/common/linear_progress_dialog.dart index 85bcc8c5b..08c46d6c9 100644 --- a/auth/lib/ui/common/linear_progress_dialog.dart +++ b/auth/lib/ui/common/linear_progress_dialog.dart @@ -1,12 +1,10 @@ - - import 'package:ente_auth/ente_theme_data.dart'; import 'package:flutter/material.dart'; class LinearProgressDialog extends StatefulWidget { final String message; - const LinearProgressDialog(this.message, {Key? key}) : super(key: key); + const LinearProgressDialog(this.message, {super.key}); @override LinearProgressDialogState createState() => LinearProgressDialogState(); @@ -29,8 +27,8 @@ class LinearProgressDialogState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, child: AlertDialog( title: Text( widget.message, diff --git a/auth/lib/ui/common/loading_widget.dart b/auth/lib/ui/common/loading_widget.dart index cf38f3e6b..d37548620 100644 --- a/auth/lib/ui/common/loading_widget.dart +++ b/auth/lib/ui/common/loading_widget.dart @@ -11,8 +11,8 @@ class EnteLoadingWidget extends StatelessWidget { this.size = 14, this.padding = 5, this.alignment = Alignment.center, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/progress_dialog.dart b/auth/lib/ui/common/progress_dialog.dart index 269a2b9b3..adabff2ee 100644 --- a/auth/lib/ui/common/progress_dialog.dart +++ b/auth/lib/ui/common/progress_dialog.dart @@ -153,8 +153,8 @@ class ProgressDialog { barrierColor: _barrierColor, builder: (BuildContext context) { _dismissingContext = context; - return WillPopScope( - onWillPop: () async => _barrierDismissible, + return PopScope( + canPop: _barrierDismissible, child: Dialog( backgroundColor: _backgroundColor, insetAnimationCurve: _insetAnimCurve, @@ -198,6 +198,7 @@ class _Body extends StatefulWidget { @override State createState() { + // ignore: no_logic_in_create_state return _dialog; } } diff --git a/auth/lib/ui/common/rename_dialog.dart b/auth/lib/ui/common/rename_dialog.dart index 03c68d3b1..ad93d1aba 100644 --- a/auth/lib/ui/common/rename_dialog.dart +++ b/auth/lib/ui/common/rename_dialog.dart @@ -8,8 +8,7 @@ class RenameDialog extends StatefulWidget { final String type; final int maxLength; - const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100}) - : super(key: key); + const RenameDialog(this.name, this.type, {super.key, this.maxLength = 100}); @override State createState() => _RenameDialogState(); diff --git a/auth/lib/ui/common/web_page.dart b/auth/lib/ui/common/web_page.dart index 838855c7f..a714bbeb2 100644 --- a/auth/lib/ui/common/web_page.dart +++ b/auth/lib/ui/common/web_page.dart @@ -6,7 +6,7 @@ class WebPage extends StatefulWidget { final String title; final String url; - const WebPage(this.title, this.url, {Key? key}) : super(key: key); + const WebPage(this.title, this.url, {super.key}); @override State createState() => _WebPageState(); @@ -28,9 +28,9 @@ class _WebPageState extends State { ), backgroundColor: Colors.black, body: InAppWebView( - initialUrlRequest: URLRequest(url: Uri.parse(widget.url)), - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(transparentBackground: true), + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + transparentBackground: true, ), onLoadStop: (c, url) { setState(() { diff --git a/auth/lib/ui/components/buttons/button_widget.dart b/auth/lib/ui/components/buttons/button_widget.dart index 28d62f4a6..1932cc02b 100644 --- a/auth/lib/ui/components/buttons/button_widget.dart +++ b/auth/lib/ui/components/buttons/button_widget.dart @@ -57,7 +57,7 @@ class ButtonWidget extends StatelessWidget { final ValueNotifier? progressStatus; const ButtonWidget({ - Key? key, + super.key, required this.buttonType, this.buttonSize = ButtonSize.large, this.icon, @@ -71,7 +71,7 @@ class ButtonWidget extends StatelessWidget { this.shouldSurfaceExecutionStates = true, this.progressStatus, this.shouldShowSuccessConfirmation = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -155,7 +155,7 @@ class ButtonChildWidget extends StatefulWidget { final bool shouldShowSuccessConfirmation; const ButtonChildWidget({ - Key? key, + super.key, required this.buttonStyle, required this.buttonType, required this.isDisabled, @@ -168,7 +168,7 @@ class ButtonChildWidget extends StatefulWidget { this.labelText, this.icon, this.buttonAction, - }) : super(key: key); + }); @override State createState() => _ButtonChildWidgetState(); diff --git a/auth/lib/ui/components/buttons/icon_button_widget.dart b/auth/lib/ui/components/buttons/icon_button_widget.dart index 9b0591d4c..eb5554318 100644 --- a/auth/lib/ui/components/buttons/icon_button_widget.dart +++ b/auth/lib/ui/components/buttons/icon_button_widget.dart @@ -17,7 +17,7 @@ class IconButtonWidget extends StatefulWidget { final Color? pressedColor; final Color? iconColor; const IconButtonWidget({ - Key? key, + super.key, required this.icon, required this.iconButtonType, this.disableGestureDetector = false, @@ -25,7 +25,7 @@ class IconButtonWidget extends StatefulWidget { this.defaultColor, this.pressedColor, this.iconColor, - }) : super(key: key); + }); @override State createState() => _IconButtonWidgetState(); diff --git a/auth/lib/ui/components/captioned_text_widget.dart b/auth/lib/ui/components/captioned_text_widget.dart index f03c8551f..438d906a9 100644 --- a/auth/lib/ui/components/captioned_text_widget.dart +++ b/auth/lib/ui/components/captioned_text_widget.dart @@ -13,8 +13,8 @@ class CaptionedTextWidget extends StatelessWidget { this.textStyle, this.makeTextBold = false, this.textColor, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/divider_widget.dart b/auth/lib/ui/components/divider_widget.dart index 489d59145..657eb024f 100644 --- a/auth/lib/ui/components/divider_widget.dart +++ b/auth/lib/ui/components/divider_widget.dart @@ -14,12 +14,12 @@ class DividerWidget extends StatelessWidget { final bool divColorHasBlur; final EdgeInsets? padding; const DividerWidget({ - Key? key, + super.key, required this.dividerType, this.bgColor = Colors.transparent, this.divColorHasBlur = true, this.padding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/expandable_menu_item_widget.dart b/auth/lib/ui/components/expandable_menu_item_widget.dart index 9628078be..0d3bce928 100644 --- a/auth/lib/ui/components/expandable_menu_item_widget.dart +++ b/auth/lib/ui/components/expandable_menu_item_widget.dart @@ -13,8 +13,8 @@ class ExpandableMenuItemWidget extends StatefulWidget { required this.title, required this.selectionOptionsWidget, required this.leadingIcon, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => diff --git a/auth/lib/ui/components/home_header_widget.dart b/auth/lib/ui/components/home_header_widget.dart index 268084eef..0079cc7fa 100644 --- a/auth/lib/ui/components/home_header_widget.dart +++ b/auth/lib/ui/components/home_header_widget.dart @@ -1,13 +1,10 @@ -import 'dart:ui'; - import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/opened_settings_event.dart'; import 'package:flutter/material.dart'; class HomeHeaderWidget extends StatefulWidget { final Widget centerWidget; - const HomeHeaderWidget({required this.centerWidget, Key? key}) - : super(key: key); + const HomeHeaderWidget({required this.centerWidget, super.key}); @override State createState() => _HomeHeaderWidgetState(); @@ -16,7 +13,7 @@ class HomeHeaderWidget extends StatefulWidget { class _HomeHeaderWidgetState extends State { @override Widget build(BuildContext context) { - final hasNotch = window.viewPadding.top > 65; + final hasNotch = View.of(context).viewPadding.top > 65; return Padding( padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4), child: Row( diff --git a/auth/lib/ui/components/menu_item_child_widgets.dart b/auth/lib/ui/components/menu_item_child_widgets.dart index 81b59bc54..1201f5e1e 100644 --- a/auth/lib/ui/components/menu_item_child_widgets.dart +++ b/auth/lib/ui/components/menu_item_child_widgets.dart @@ -12,7 +12,7 @@ class TrailingWidget extends StatefulWidget { final double trailingExtraMargin; final bool showExecutionStates; const TrailingWidget({ - Key? key, + super.key, required this.executionStateNotifier, this.trailingIcon, this.trailingIconColor, @@ -20,7 +20,7 @@ class TrailingWidget extends StatefulWidget { required this.trailingIconIsMuted, required this.trailingExtraMargin, required this.showExecutionStates, - }) : super(key: key); + }); @override State createState() => _TrailingWidgetState(); } @@ -101,11 +101,11 @@ class ExpansionTrailingIcon extends StatelessWidget { final IconData? trailingIcon; final Color? trailingIconColor; const ExpansionTrailingIcon({ - Key? key, + super.key, required this.isExpanded, this.trailingIcon, this.trailingIconColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -138,12 +138,12 @@ class LeadingWidget extends StatelessWidget { // leadIconSize deafult value is 20. final double leadingIconSize; const LeadingWidget({ - Key? key, + super.key, required this.leadingIconSize, this.leadingIcon, this.leadingIconColor, this.leadingIconWidget, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/menu_item_widget.dart b/auth/lib/ui/components/menu_item_widget.dart index ef2249f2a..f281edd33 100644 --- a/auth/lib/ui/components/menu_item_widget.dart +++ b/auth/lib/ui/components/menu_item_widget.dart @@ -86,8 +86,8 @@ class MenuItemWidget extends StatefulWidget { this.showOnlyLoadingState = false, this.surfaceExecutionStates = true, this.alwaysShowSuccessState = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _MenuItemWidgetState(); diff --git a/auth/lib/ui/components/menu_section_description_widget.dart b/auth/lib/ui/components/menu_section_description_widget.dart index 6e521a536..bcbb9f2fa 100644 --- a/auth/lib/ui/components/menu_section_description_widget.dart +++ b/auth/lib/ui/components/menu_section_description_widget.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class MenuSectionDescriptionWidget extends StatelessWidget { final String content; - const MenuSectionDescriptionWidget({Key? key, required this.content}) - : super(key: key); + const MenuSectionDescriptionWidget({super.key, required this.content}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/notification_warning_widget.dart b/auth/lib/ui/components/notification_warning_widget.dart index a799ffcd3..ab5e6dec1 100644 --- a/auth/lib/ui/components/notification_warning_widget.dart +++ b/auth/lib/ui/components/notification_warning_widget.dart @@ -21,14 +21,14 @@ class NotificationWidget extends StatelessWidget { final NotificationType type; const NotificationWidget({ - Key? key, + super.key, required this.startIcon, required this.actionIcon, required this.text, required this.onTap, this.subText, this.type = NotificationType.warning, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/title_bar_title_widget.dart b/auth/lib/ui/components/title_bar_title_widget.dart index bc20fe5b1..0aa2534aa 100644 --- a/auth/lib/ui/components/title_bar_title_widget.dart +++ b/auth/lib/ui/components/title_bar_title_widget.dart @@ -6,11 +6,11 @@ class TitleBarTitleWidget extends StatelessWidget { final bool isTitleH2; final IconData? icon; const TitleBarTitleWidget({ - Key? key, + super.key, this.title, this.isTitleH2 = false, this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/title_bar_widget.dart b/auth/lib/ui/components/title_bar_widget.dart index 8e22d4f65..16b532b96 100644 --- a/auth/lib/ui/components/title_bar_widget.dart +++ b/auth/lib/ui/components/title_bar_widget.dart @@ -14,7 +14,7 @@ class TitleBarWidget extends StatelessWidget { final bool isOnTopOfScreen; final Color? backgroundColor; const TitleBarWidget({ - Key? key, + super.key, this.leading, this.title, this.caption, @@ -25,7 +25,7 @@ class TitleBarWidget extends StatelessWidget { this.isFlexibleSpaceDisabled = false, this.isOnTopOfScreen = true, this.backgroundColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/toggle_switch_widget.dart b/auth/lib/ui/components/toggle_switch_widget.dart index 8ae99f77b..3fc0d153c 100644 --- a/auth/lib/ui/components/toggle_switch_widget.dart +++ b/auth/lib/ui/components/toggle_switch_widget.dart @@ -13,8 +13,8 @@ class ToggleSwitchWidget extends StatefulWidget { const ToggleSwitchWidget({ required this.value, required this.onChanged, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ToggleSwitchWidgetState(); diff --git a/auth/lib/ui/home/coach_mark_widget.dart b/auth/lib/ui/home/coach_mark_widget.dart index f628fabd4..b0e15d7d2 100644 --- a/auth/lib/ui/home/coach_mark_widget.dart +++ b/auth/lib/ui/home/coach_mark_widget.dart @@ -21,37 +21,43 @@ class CoachMarkWidget extends StatelessWidget { children: [ Expanded( child: Container( + width: double.infinity, color: Theme.of(context).colorScheme.background.withOpacity(0.1), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), - child: Column( + child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon( - Icons.swipe_left, - size: 42, - ), - const SizedBox( - height: 12, - ), - Text( - l10n.swipeHint, - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - const SizedBox( - height: 16, - ), - SizedBox( - width: 160, - child: OutlinedButton( - onPressed: () async { - await PreferenceService.instance - .setHasShownCoachMark(true); - Bus.instance.fire(CodesUpdatedEvent()); - }, - child: Text(l10n.ok), - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.swipe_left, + size: 42, + ), + const SizedBox( + height: 24, + ), + Text( + l10n.swipeHint, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox( + height: 36, + ), + SizedBox( + width: 160, + child: OutlinedButton( + onPressed: () async { + await PreferenceService.instance + .setHasShownCoachMark(true); + Bus.instance.fire(CodesUpdatedEvent()); + }, + child: Text(l10n.ok), + ), + ), + ], ), ], ), diff --git a/auth/lib/ui/home/home_empty_state.dart b/auth/lib/ui/home/home_empty_state.dart index 8a185beef..0768f0491 100644 --- a/auth/lib/ui/home/home_empty_state.dart +++ b/auth/lib/ui/home/home_empty_state.dart @@ -3,6 +3,7 @@ import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; import 'package:ente_auth/ui/settings/faq.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; class HomeEmptyStateWidget extends StatelessWidget { @@ -10,86 +11,90 @@ class HomeEmptyStateWidget extends StatelessWidget { final VoidCallback? onManuallySetupTap; const HomeEmptyStateWidget({ - Key? key, + super.key, required this.onScanTap, required this.onManuallySetupTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final l10n = context.l10n; return SingleChildScrollView( child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - children: [ - Image.asset( - "assets/wallet-front-gradient.png", - width: 200, - height: 200, - ), - Text( - l10n.setupFirstAccount, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 64), - SizedBox( - width: 400, - child: OutlinedButton( - onPressed: onScanTap, - child: Text(l10n.importScanQrCode), + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor(height: 800, width: 450), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Image.asset( + "assets/wallet-front-gradient.png", + width: 200, + height: 200, ), - ), - const SizedBox(height: 18), - SizedBox( - width: 400, - child: OutlinedButton( - onPressed: onManuallySetupTap, - child: Text(l10n.importEnterSetupKey), - ), - ), - const SizedBox(height: 54), - InkWell( - onTap: () { - routeToPage(context, ImportCodePage()); - }, - child: Text( - l10n.importCodes, + Text( + l10n.setupFirstAccount, textAlign: TextAlign.center, - style: getEnteTextTheme(context) - .bodyFaint - .copyWith(decoration: TextDecoration.underline), + style: Theme.of(context).textTheme.headlineMedium, ), - ), - const SizedBox(height: 18), - InkWell( - onTap: () { - showModalBottomSheet( - backgroundColor: - Theme.of(context).colorScheme.background, - barrierColor: Colors.black87, - context: context, - builder: (context) { - return const FAQQuestionsWidget(); - }, - ); - }, - child: Text( - l10n.faq, - textAlign: TextAlign.center, - style: getEnteTextTheme(context) - .bodyFaint - .copyWith(decoration: TextDecoration.underline), + const SizedBox(height: 64), + if (PlatformUtil.isMobile()) + SizedBox( + width: 400, + child: OutlinedButton( + onPressed: onScanTap, + child: Text(l10n.importScanQrCode), + ), + ), + const SizedBox(height: 18), + SizedBox( + width: 400, + child: OutlinedButton( + onPressed: onManuallySetupTap, + child: Text(l10n.importEnterSetupKey), + ), ), - ), - ], - ), - ], + const SizedBox(height: 54), + InkWell( + onTap: () { + routeToPage(context, const ImportCodePage()); + }, + child: Text( + l10n.importCodes, + textAlign: TextAlign.center, + style: getEnteTextTheme(context) + .bodyFaint + .copyWith(decoration: TextDecoration.underline), + ), + ), + const SizedBox(height: 18), + InkWell( + onTap: () { + showModalBottomSheet( + backgroundColor: + Theme.of(context).colorScheme.background, + barrierColor: Colors.black87, + context: context, + builder: (context) { + return const FAQQuestionsWidget(); + }, + ); + }, + child: Text( + l10n.faq, + textAlign: TextAlign.center, + style: getEnteTextTheme(context) + .bodyFaint + .copyWith(decoration: TextDecoration.underline), + ), + ), + ], + ), + ], + ), ), ), ), diff --git a/auth/lib/ui/home/speed_dial_label_widget.dart b/auth/lib/ui/home/speed_dial_label_widget.dart index 40c945893..4889ffb6a 100644 --- a/auth/lib/ui/home/speed_dial_label_widget.dart +++ b/auth/lib/ui/home/speed_dial_label_widget.dart @@ -6,8 +6,8 @@ class SpeedDialLabelWidget extends StatelessWidget { const SpeedDialLabelWidget( this.label, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index 792a4c917..341e1ae69 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:app_links/app_links.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/ente_theme_data.dart'; @@ -22,28 +23,32 @@ import 'package:ente_auth/ui/home/speed_dial_label_widget.dart'; import 'package:ente_auth/ui/scanner_page.dart'; import 'package:ente_auth/ui/settings_page.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/totp_util.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; -import 'package:uni_links/uni_links.dart'; class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { - static final _settingsPage = SettingsPage( + late final _settingsPage = SettingsPage( emailNotifier: UserService.instance.emailValueNotifier, + scaffoldKey: scaffoldKey, ); bool _hasLoaded = false; bool _isSettingsOpen = false; final Logger _logger = Logger("HomePage"); + final scaffoldKey = GlobalKey(); final TextEditingController _textController = TextEditingController(); bool _showSearchBox = false; @@ -144,28 +149,24 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - return WillPopScope( - onWillPop: () async { + return PopScope( + onPopInvoked: (_) async { if (_isSettingsOpen) { - Navigator.pop(context); - return false; - } - if (Platform.isAndroid) { - // ignore: unawaited_futures - MoveToBackground.moveTaskToBack(); - return false; - } else { - return true; + scaffoldKey.currentState!.closeDrawer(); + return; + } else if (!Platform.isAndroid) { + Navigator.of(context).pop(); + return; } + await MoveToBackground.moveTaskToBack(); }, + canPop: false, child: Scaffold( + key: scaffoldKey, drawerEnableOpenDragGesture: !Platform.isAndroid, - drawer: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 428), - child: Drawer( - width: double.infinity, - child: _settingsPage, - ), + drawer: Drawer( + width: 428, + child: _settingsPage, ), onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened, body: SafeArea( @@ -179,7 +180,7 @@ class _HomePageState extends State { resizeToAvoidBottomInset: false, appBar: AppBar( title: !_showSearchBox - ? const Text('ente Auth') + ? const Text('Ente Auth') : TextField( autofocus: _searchText.isEmpty, controller: _textController, @@ -205,6 +206,7 @@ class _HomePageState extends State { _showSearchBox = !_showSearchBox; if (!_showSearchBox) { _textController.clear(); + _searchText = ""; } else { _searchText = _textController.text; } @@ -233,10 +235,13 @@ class _HomePageState extends State { onManuallySetupTap: _redirectToManualEntryPage, ); } else { - final list = ListView.builder( + final list = AlignedGridView.count( + crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400) + .clamp(1, double.infinity) + .toInt(), itemBuilder: ((context, index) { try { - return CodeWidget(_filteredCodes[index]); + return ClipRect(child: CodeWidget(_filteredCodes[index])); } catch (e) { return const Text("Failed"); } @@ -255,7 +260,11 @@ class _HomePageState extends State { children: [ Expanded( child: _filteredCodes.isNotEmpty - ? ListView.builder( + ? AlignedGridView.count( + crossAxisCount: + (MediaQuery.sizeOf(context).width ~/ 400) + .clamp(1, double.infinity) + .toInt(), itemBuilder: ((context, index) { Code? code; try { @@ -290,8 +299,10 @@ class _HomePageState extends State { Future _initDeepLinks() async { // Platform messages may fail, so we use a try/catch PlatformException. + final appLinks = AppLinks(); try { - final String? initialLink = await getInitialLink(); + String? initialLink; + initialLink = await appLinks.getInitialAppLinkString(); // Parse the link and warn the user, if it is not correct, // but keep in mind it could be `null`. if (initialLink != null) { @@ -307,14 +318,16 @@ class _HomePageState extends State { } // Attach a listener to the stream - linkStream.listen( - (String? link) { - _handleDeeplink(context, link); - }, - onError: (err) { - _logger.severe(err); - }, - ); + if (!kIsWeb && !Platform.isLinux) { + appLinks.stringLinkStream.listen( + (link) { + _handleDeeplink(context, link); + }, + onError: (err) { + _logger.severe(err); + }, + ); + } return false; } @@ -343,6 +356,14 @@ class _HomePageState extends State { } Widget _getFab() { + if (PlatformUtil.isDesktop()) { + return FloatingActionButton( + onPressed: () => _redirectToManualEntryPage(), + child: const Icon(Icons.add), + elevation: 8.0, + shape: const CircleBorder(), + ); + } return SpeedDial( icon: Icons.add, activeIcon: Icons.close, diff --git a/auth/lib/ui/linear_progress_widget.dart b/auth/lib/ui/linear_progress_widget.dart index 6296e5d4a..a5dbb2140 100644 --- a/auth/lib/ui/linear_progress_widget.dart +++ b/auth/lib/ui/linear_progress_widget.dart @@ -6,8 +6,8 @@ class LinearProgressWidget extends StatelessWidget { const LinearProgressWidget({ required this.color, required this.fractionOfStorage, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/passkey_page.dart b/auth/lib/ui/passkey_page.dart index 3c60af36e..8c2e54e98 100644 --- a/auth/lib/ui/passkey_page.dart +++ b/auth/lib/ui/passkey_page.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:app_links/app_links.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/account/two_factor.dart'; @@ -9,7 +10,6 @@ import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher_string.dart'; class PasskeyPage extends StatefulWidget { @@ -17,8 +17,8 @@ class PasskeyPage extends StatefulWidget { const PasskeyPage( this.sessionID, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PasskeyPageState(); @@ -77,8 +77,9 @@ class _PasskeyPageState extends State { } Future _initDeepLinks() async { + final appLinks = AppLinks(); // Attach a listener to the stream - linkStream.listen( + appLinks.stringLinkStream.listen( _handleDeeplink, onError: (err) { _logger.severe(err); diff --git a/auth/lib/ui/scanner_gauth_page.dart b/auth/lib/ui/scanner_gauth_page.dart index 785f1d3d3..41d0a762d 100644 --- a/auth/lib/ui/scanner_gauth_page.dart +++ b/auth/lib/ui/scanner_gauth_page.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class ScannerGoogleAuthPage extends StatefulWidget { - const ScannerGoogleAuthPage({Key? key}) : super(key: key); + const ScannerGoogleAuthPage({super.key}); @override State createState() => ScannerGoogleAuthPageState(); @@ -85,7 +85,7 @@ class ScannerGoogleAuthPageState extends State { } catch (e) { controller.dispose(); Navigator.of(context).pop(); - showToast(context, "Error " + e.toString()); + showToast(context, "Error $e"); } }); } diff --git a/auth/lib/ui/scanner_page.dart b/auth/lib/ui/scanner_page.dart index 0159f0cfa..6a7793631 100644 --- a/auth/lib/ui/scanner_page.dart +++ b/auth/lib/ui/scanner_page.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class ScannerPage extends StatefulWidget { - const ScannerPage({Key? key}) : super(key: key); + const ScannerPage({super.key}); @override State createState() => ScannerPageState(); diff --git a/auth/lib/ui/settings/about_section_widget.dart b/auth/lib/ui/settings/about_section_widget.dart index 4fcd2c27c..a96e1f0ad 100644 --- a/auth/lib/ui/settings/about_section_widget.dart +++ b/auth/lib/ui/settings/about_section_widget.dart @@ -1,19 +1,19 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/app_update_dialog.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutSectionWidget extends StatelessWidget { - const AboutSectionWidget({Key? key}) : super(key: key); + const AboutSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -104,8 +104,8 @@ class AboutMenuItemWidget extends StatelessWidget { required this.title, required this.url, this.webPageTitle, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -117,13 +117,10 @@ class AboutMenuItemWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - // ignore: unawaited_futures - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage(webPageTitle ?? title, url); - }, - ), + await PlatformUtil.openWebView( + context, + webPageTitle ?? title, + url, ); }, ); diff --git a/auth/lib/ui/settings/account_section_widget.dart b/auth/lib/ui/settings/account_section_widget.dart index 60e426b09..d51b2dd87 100644 --- a/auth/lib/ui/settings/account_section_widget.dart +++ b/auth/lib/ui/settings/account_section_widget.dart @@ -13,11 +13,12 @@ import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class AccountSectionWidget extends StatelessWidget { - AccountSectionWidget({Key? key}) : super(key: key); + AccountSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -47,6 +48,7 @@ class AccountSectionWidget extends StatelessWidget { context, l10n.authToChangeYourEmail, ); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { // ignore: unawaited_futures showDialog( @@ -106,7 +108,7 @@ class AccountSectionWidget extends StatelessWidget { String recoveryKey; try { recoveryKey = - Sodium.bin2hex(Configuration.instance.getRecoveryKey()); + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); } catch (e) { // ignore: unawaited_futures showGenericErrorDialog(context: context); diff --git a/auth/lib/ui/settings/app_update_dialog.dart b/auth/lib/ui/settings/app_update_dialog.dart index 12049017c..176abc4b1 100644 --- a/auth/lib/ui/settings/app_update_dialog.dart +++ b/auth/lib/ui/settings/app_update_dialog.dart @@ -1,13 +1,14 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; class AppUpdateDialog extends StatefulWidget { final LatestVersionInfo? latestVersionInfo; - const AppUpdateDialog(this.latestVersionInfo, {Key? key}) : super(key: key); + const AppUpdateDialog(this.latestVersionInfo, {super.key}); @override State createState() => _AppUpdateDialogState(); @@ -23,7 +24,7 @@ class _AppUpdateDialogState extends State { Padding( padding: const EdgeInsets.fromLTRB(8, 4, 0, 4), child: Text( - "- " + log, + "- $log", style: Theme.of(context).textTheme.bodySmall!.copyWith( fontSize: 14, ), @@ -68,7 +69,9 @@ class _AppUpdateDialogState extends State { ), ), onPressed: () => launchUrlString( - widget.latestVersionInfo!.url!, + PlatformUtil.isDesktop() + ? widget.latestVersionInfo!.release! + : widget.latestVersionInfo!.url!, mode: LaunchMode.externalApplication, ), child: Text( @@ -80,8 +83,8 @@ class _AppUpdateDialogState extends State { ); final shouldForceUpdate = UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo); - return WillPopScope( - onWillPop: () async => !shouldForceUpdate, + return PopScope( + canPop: !shouldForceUpdate, child: AlertDialog( title: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/auth/lib/ui/settings/app_version_widget.dart b/auth/lib/ui/settings/app_version_widget.dart index 3b15c6480..58c693f7c 100644 --- a/auth/lib/ui/settings/app_version_widget.dart +++ b/auth/lib/ui/settings/app_version_widget.dart @@ -4,8 +4,8 @@ import 'package:package_info_plus/package_info_plus.dart'; class AppVersionWidget extends StatefulWidget { const AppVersionWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _AppVersionWidgetState(); @@ -48,7 +48,7 @@ class _AppVersionWidgetState extends State { return Padding( padding: const EdgeInsets.all(20), child: Text( - "Version: " + snapshot.data!, + "Version: ${snapshot.data!}", style: Theme.of(context).textTheme.bodySmall, ), ); diff --git a/auth/lib/ui/settings/danger_section_widget.dart b/auth/lib/ui/settings/danger_section_widget.dart index 2e7eccc16..4f8160c38 100644 --- a/auth/lib/ui/settings/danger_section_widget.dart +++ b/auth/lib/ui/settings/danger_section_widget.dart @@ -11,7 +11,7 @@ import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/material.dart'; class DangerSectionWidget extends StatelessWidget { - const DangerSectionWidget({Key? key}) : super(key: key); + const DangerSectionWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/data/data_section_widget.dart b/auth/lib/ui/settings/data/data_section_widget.dart index f9c3ed117..f32739d23 100644 --- a/auth/lib/ui/settings/data/data_section_widget.dart +++ b/auth/lib/ui/settings/data/data_section_widget.dart @@ -10,7 +10,9 @@ import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/material.dart'; class DataSectionWidget extends StatelessWidget { - DataSectionWidget({Key? key}) : super(key: key); + // final _logger = Logger("AccountSectionWidget"); + + DataSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -35,8 +37,7 @@ class DataSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - // ignore: unawaited_futures - routeToPage(context, ImportCodePage()); + await routeToPage(context, const ImportCodePage()); }, ), sectionOptionSpacing, diff --git a/auth/lib/ui/settings/data/export_widget.dart b/auth/lib/ui/settings/data/export_widget.dart index c9066ccce..ef438301c 100644 --- a/auth/lib/ui/settings/data/export_widget.dart +++ b/auth/lib/ui/settings/data/export_widget.dart @@ -9,14 +9,14 @@ import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:share_plus/share_plus.dart'; @@ -76,17 +76,17 @@ Future _requestForEncryptionPassword( try { final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); String exportPlainText = await _getAuthDataForExport(); // Encrypt the key with this derived key - final encResult = await CryptoUtil.encryptChaCha( - utf8.encode(exportPlainText) as Uint8List, + final encResult = await CryptoUtil.encryptData( + utf8.encode(exportPlainText), derivedKeyResult.key, ); - final encContent = Sodium.bin2base64(encResult.encryptedData!); - final encNonce = Sodium.bin2base64(encResult.header!); + final encContent = CryptoUtil.bin2base64(encResult.encryptedData!); + final encNonce = CryptoUtil.bin2base64(encResult.header!); final EnteAuthExport data = EnteAuthExport( version: 1, encryptedData: encContent, @@ -94,7 +94,7 @@ Future _requestForEncryptionPassword( kdfParams: KDFParams( memLimit: derivedKeyResult.memLimit, opsLimit: derivedKeyResult.opsLimit, - salt: Sodium.bin2base64(kekSalt), + salt: CryptoUtil.bin2base64(kekSalt), ), ); // get json value of data @@ -126,46 +126,55 @@ Future _showExportWarningDialog(BuildContext context) async { Future _exportCodes(BuildContext context, String fileContent) async { DateTime now = DateTime.now().toUtc(); String formattedDate = DateFormat('yyyy-MM-dd').format(now); - String exportFileName = 'ente-auth-codes-$formattedDate.txt'; - final _codeFile = File( - Configuration.instance.getTempDirectory() + exportFileName, - ); + String exportFileName = 'ente-auth-codes-$formattedDate'; + String exportFileExtension = 'txt'; final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.authToExportCodes); + await PlatformUtil.refocusWindows(); if (!hasAuthenticated) { return; } - if (_codeFile.existsSync()) { - await _codeFile.delete(); - } - _codeFile.writeAsStringSync(fileContent); - final Size size = MediaQuery.of(context).size; - - if (Platform.isAndroid) { - await FileSaver.instance.saveAs( - name: exportFileName, - filePath: _codeFile.path, - mimeType: MimeType.text, - ext: 'txt', - ); - } else { - await Share.shareFiles( - [_codeFile.path], - sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2), - ); - } - Future.delayed(const Duration(seconds: 30), () async { - if (_codeFile.existsSync()) { - _codeFile.deleteSync(); - } - }); + Future.delayed( + const Duration(milliseconds: 1200), + () async => await shareDialog( + context, + context.l10n.exportCodes, + saveAction: () async { + await PlatformUtil.shareFile( + exportFileName, + exportFileExtension, + CryptoUtil.strToBin(fileContent), + MimeType.text, + ); + }, + sendAction: () async { + final codeFile = File( + "${Configuration.instance.getTempDirectory()}$exportFileName.$exportFileExtension", + ); + if (codeFile.existsSync()) { + await codeFile.delete(); + } + codeFile.writeAsStringSync(fileContent); + final Size size = MediaQuery.of(context).size; + await Share.shareXFiles( + [XFile(codeFile.path)], + sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2), + ); + Future.delayed(const Duration(seconds: 30), () async { + if (codeFile.existsSync()) { + codeFile.deleteSync(); + } + }); + }, + ), + ); } Future _getAuthDataForExport() async { final codes = await CodeStore.instance.getAllCodes(); String data = ""; for (final code in codes) { - data += code.rawData + "\n"; + data += "${code.rawData}\n"; } return data; } diff --git a/auth/lib/ui/settings/data/import/aegis_import.dart b/auth/lib/ui/settings/data/import/aegis_import.dart index 5042d618f..b801e64a5 100644 --- a/auth/lib/ui/settings/data/import/aegis_import.dart +++ b/auth/lib/ui/settings/data/import/aegis_import.dart @@ -91,7 +91,7 @@ Future _processAegisExportFile( final jsonString = await file.readAsString(); final decodedJson = jsonDecode(jsonString); final isEncrypted = decodedJson['header']['slots'] != null; - var aegisDB; + Map? aegisDB; if (isEncrypted) { String? password; try { @@ -127,7 +127,7 @@ Future _processAegisExportFile( aegisDB = decodedJson['db']; } final parsedCodes = []; - for (var item in aegisDB['entries']) { + for (var item in aegisDB?['entries']) { var kind = item['type']; var account = item['name']; var issuer = item['issuer']; diff --git a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart index 15f869206..511c9bbf9 100644 --- a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart +++ b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart @@ -11,21 +11,20 @@ import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/settings/data/import/import_success.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; Future showEncryptedImportInstruction(BuildContext context) async { final l10n = context.l10n; final result = await showDialogWidget( context: context, - title: l10n.importFromApp("ente Auth"), + title: l10n.importFromApp("Ente Auth"), body: l10n.importEnteEncGuide, buttons: [ ButtonWidget( @@ -80,21 +79,21 @@ Future _decryptExportData( try { await progressDialog.show(); final derivedKey = await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, - Sodium.base642bin(enteAuthExport.kdfParams.salt), + utf8.encode(password), + CryptoUtil.base642bin(enteAuthExport.kdfParams.salt), enteAuthExport.kdfParams.memLimit, enteAuthExport.kdfParams.opsLimit, ); Uint8List? decryptedContent; // Encrypt the key with this derived key try { - decryptedContent = await CryptoUtil.decryptChaCha( - Sodium.base642bin(enteAuthExport.encryptedData), + decryptedContent = await CryptoUtil.decryptData( + CryptoUtil.base642bin(enteAuthExport.encryptedData), derivedKey, - Sodium.base642bin(enteAuthExport.encryptionNonce), + CryptoUtil.base642bin(enteAuthExport.encryptionNonce), ); - } catch (e,s) { - Logger("encryptedImport").warning('failed to decrypt',e,s); + } catch (e, s) { + Logger("encryptedImport").warning('failed to decrypt', e, s); showToast(context, l10n.incorrectPasswordTitle); isPasswordIncorrect = true; } diff --git a/auth/lib/ui/settings/data/import/google_auth_import.dart b/auth/lib/ui/settings/data/import/google_auth_import.dart index 2c83bc206..12df41a14 100644 --- a/auth/lib/ui/settings/data/import/google_auth_import.dart +++ b/auth/lib/ui/settings/data/import/google_auth_import.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:base32/base32.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; @@ -12,6 +11,8 @@ import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/scanner_gauth_page.dart'; import 'package:ente_auth/ui/settings/data/import/import_success.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -24,13 +25,14 @@ Future showGoogleAuthInstruction(BuildContext context) async { title: l10n.importFromApp("Google Authenticator"), body: l10n.importGoogleAuthGuide, buttons: [ - ButtonWidget( - buttonType: ButtonType.primary, - labelText: l10n.scanAQrCode, - isInAlert: true, - buttonSize: ButtonSize.large, - buttonAction: ButtonAction.first, - ), + if (PlatformUtil.isMobile()) + ButtonWidget( + buttonType: ButtonType.primary, + labelText: l10n.scanAQrCode, + isInAlert: true, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + ), ButtonWidget( buttonType: ButtonType.secondary, labelText: context.l10n.cancel, diff --git a/auth/lib/ui/settings/data/import/import_service.dart b/auth/lib/ui/settings/data/import/import_service.dart index 0e42197ba..a315c1679 100644 --- a/auth/lib/ui/settings/data/import/import_service.dart +++ b/auth/lib/ui/settings/data/import/import_service.dart @@ -1,4 +1,3 @@ -import 'package:ente_auth/ui/settings/data/import/2fas_import.dart'; import 'package:ente_auth/ui/settings/data/import/aegis_import.dart'; import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart'; import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart'; @@ -6,6 +5,7 @@ import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart'; import 'package:ente_auth/ui/settings/data/import/lastpass_import.dart'; import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart'; import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart'; +import 'package:ente_auth/ui/settings/data/import/two_fas_import.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; import 'package:flutter/cupertino.dart'; diff --git a/auth/lib/ui/settings/data/import/import_success.dart b/auth/lib/ui/settings/data/import/import_success.dart index cc77248e8..5827248f8 100644 --- a/auth/lib/ui/settings/data/import/import_success.dart +++ b/auth/lib/ui/settings/data/import/import_success.dart @@ -11,9 +11,9 @@ Future importSuccessDialog(BuildContext context, int count) async { firstButtonLabel: context.l10n.ok, firstButtonOnTap: () async { Navigator.of(context).pop(); - if(Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } + // if(Navigator.of(context).canPop()) { + // Navigator.of(context).pop(); + // } }, firstButtonType: ButtonType.primary, ); diff --git a/auth/lib/ui/settings/data/import/plain_text_import.dart b/auth/lib/ui/settings/data/import/plain_text_import.dart index a8e64bb5f..03bc50dce 100644 --- a/auth/lib/ui/settings/data/import/plain_text_import.dart +++ b/auth/lib/ui/settings/data/import/plain_text_import.dart @@ -1,6 +1,6 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; -import 'dart:ui'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; @@ -48,10 +48,8 @@ class PlainTextImport extends StatelessWidget { ], ); } - } - Future showImportInstructionDialog(BuildContext context) async { final l10n = context.l10n; final AlertDialog alert = AlertDialog( @@ -94,7 +92,6 @@ Future showImportInstructionDialog(BuildContext context) async { ); } - Future _pickImportFile(BuildContext context) async { final l10n = context.l10n; FilePickerResult? result = await FilePicker.platform.pickFiles(); @@ -108,7 +105,7 @@ Future _pickImportFile(BuildContext context) async { final codes = await file.readAsString(); List splitCodes = codes.split(","); if (splitCodes.length == 1) { - splitCodes = codes.split("\n"); + splitCodes = const LineSplitter().convert(codes); } final parsedCodes = []; for (final code in splitCodes) { diff --git a/auth/lib/ui/settings/data/import/2fas_import.dart b/auth/lib/ui/settings/data/import/two_fas_import.dart similarity index 98% rename from auth/lib/ui/settings/data/import/2fas_import.dart rename to auth/lib/ui/settings/data/import/two_fas_import.dart index b6e75f3c6..ae5a05b0b 100644 --- a/auth/lib/ui/settings/data/import/2fas_import.dart +++ b/auth/lib/ui/settings/data/import/two_fas_import.dart @@ -170,8 +170,8 @@ Future _process2FasExportFile( } String decrypt2FasVault(dynamic data, {required String password}) { - int ITERATION_COUNT = 10000; - int KEY_SIZE = 256; + int iterationCount = 10000; + int keySize = 256; final String encryptedServices = data["servicesEncrypted"]; var split = encryptedServices.split(":"); final encryptedData = base64.decode(split[0]); @@ -181,11 +181,11 @@ String decrypt2FasVault(dynamic data, {required String password}) { final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)); final params = Pbkdf2Parameters( salt, - ITERATION_COUNT, - KEY_SIZE ~/ 8, + iterationCount, + keySize ~/ 8, ); pbkdf2.init(params); - Uint8List key = Uint8List(KEY_SIZE ~/ 8); + Uint8List key = Uint8List(keySize ~/ 8); pbkdf2.deriveKey(Uint8List.fromList(utf8.encode(password)), 0, key, 0); final decrypted = decrypt(key, iv, encryptedData); final utf8Decode = utf8.decode(decrypted); diff --git a/auth/lib/ui/settings/data/import_page.dart b/auth/lib/ui/settings/data/import_page.dart index cd0f2114a..c5ad2206e 100644 --- a/auth/lib/ui/settings/data/import_page.dart +++ b/auth/lib/ui/settings/data/import_page.dart @@ -21,7 +21,9 @@ enum ImportType { } class ImportCodePage extends StatelessWidget { - final List importOptions = [ + const ImportCodePage({super.key}); + + static const List importOptions = [ ImportType.plainText, ImportType.encrypted, ImportType.twoFas, @@ -32,8 +34,6 @@ class ImportCodePage extends StatelessWidget { ImportType.lastpass, ]; - ImportCodePage({super.key}); - String getTitle(BuildContext context, ImportType type) { switch (type) { case ImportType.plainText: diff --git a/auth/lib/ui/settings/debug_section_widget.dart b/auth/lib/ui/settings/debug_section_widget.dart index 259937ff7..03406f791 100644 --- a/auth/lib/ui/settings/debug_section_widget.dart +++ b/auth/lib/ui/settings/debug_section_widget.dart @@ -3,9 +3,9 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/ui/settings/settings_section_title.dart'; import 'package:ente_auth/ui/settings/settings_text_item.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class DebugSectionWidget extends StatelessWidget { const DebugSectionWidget({super.key}); @@ -51,7 +51,7 @@ class DebugSectionWidget extends StatelessWidget { "Key", style: TextStyle(fontWeight: FontWeight.bold), ), - Text(Sodium.bin2base64(Configuration.instance.getKey()!)), + Text(CryptoUtil.bin2base64(Configuration.instance.getKey()!)), const Padding(padding: EdgeInsets.all(12)), const Text( "Encrypted Key", diff --git a/auth/lib/ui/settings/developer_settings_page.dart b/auth/lib/ui/settings/developer_settings_page.dart index 96139e982..5bd72be40 100644 --- a/auth/lib/ui/settings/developer_settings_page.dart +++ b/auth/lib/ui/settings/developer_settings_page.dart @@ -11,7 +11,7 @@ class DeveloperSettingsPage extends StatefulWidget { const DeveloperSettingsPage({super.key}); @override - _DeveloperSettingsPageState createState() => _DeveloperSettingsPageState(); + State createState() => _DeveloperSettingsPageState(); } class _DeveloperSettingsPageState extends State { @@ -27,7 +27,7 @@ class _DeveloperSettingsPageState extends State { @override Widget build(BuildContext context) { _logger.info( - "Current endpoint is: " + Configuration.instance.getHttpEndpoint(), + "Current endpoint is: ${Configuration.instance.getHttpEndpoint()}", ); return Scaffold( appBar: AppBar( @@ -49,7 +49,7 @@ class _DeveloperSettingsPageState extends State { GradientButton( onTap: () async { String url = _urlController.text; - _logger.info("Entered endpoint: " + url); + _logger.info("Entered endpoint: $url"); try { final uri = Uri.parse(url); if ((uri.scheme == "http" || uri.scheme == "https")) { @@ -79,7 +79,7 @@ class _DeveloperSettingsPageState extends State { Future _ping(String endpoint) async { try { - final response = await Dio().get(endpoint + '/ping'); + final response = await Dio().get('$endpoint/ping'); if (response.data['message'] != 'pong') { throw Exception('Invalid response'); } diff --git a/auth/lib/ui/settings/developer_settings_widget.dart b/auth/lib/ui/settings/developer_settings_widget.dart index 0fb32301c..32f05f929 100644 --- a/auth/lib/ui/settings/developer_settings_widget.dart +++ b/auth/lib/ui/settings/developer_settings_widget.dart @@ -15,7 +15,7 @@ class DeveloperSettingsWidget extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Text( context.l10n.customEndpoint( - endpointURI.host + ":" + endpointURI.port.toString(), + "${endpointURI.host}:${endpointURI.port}", ), style: Theme.of(context).textTheme.bodySmall, ), diff --git a/auth/lib/ui/settings/faq.dart b/auth/lib/ui/settings/faq.dart index d9aa88f1b..5f441912e 100644 --- a/auth/lib/ui/settings/faq.dart +++ b/auth/lib/ui/settings/faq.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; class FAQQuestionsWidget extends StatelessWidget { const FAQQuestionsWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -64,9 +64,9 @@ class FAQQuestionsWidget extends StatelessWidget { class FaqWidget extends StatelessWidget { const FaqWidget({ - Key? key, + super.key, required this.faq, - }) : super(key: key); + }); final FaqItem? faq; diff --git a/auth/lib/ui/settings/general_section_widget.dart b/auth/lib/ui/settings/general_section_widget.dart index 4518be8de..2b74cbf80 100644 --- a/auth/lib/ui/settings/general_section_widget.dart +++ b/auth/lib/ui/settings/general_section_widget.dart @@ -17,7 +17,7 @@ import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; class AdvancedSectionWidget extends StatefulWidget { - const AdvancedSectionWidget({Key? key}) : super(key: key); + const AdvancedSectionWidget({super.key}); @override State createState() => _AdvancedSectionWidgetState(); diff --git a/auth/lib/ui/settings/language_picker.dart b/auth/lib/ui/settings/language_picker.dart index 997e53068..1d0aa32e8 100644 --- a/auth/lib/ui/settings/language_picker.dart +++ b/auth/lib/ui/settings/language_picker.dart @@ -18,53 +18,58 @@ class LanguageSelectorPage extends StatelessWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( - body: CustomScrollView( - primary: false, - slivers: [ - TitleBarWidget( - flexibleSpaceTitle: TitleBarTitleWidget( - title: l10n.selectLanguage, - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 20, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - child: ItemsWidget( - supportedLocales, - onLocaleChanged, - currentLocale, - ), + body: Center( + child: Container( + constraints: const BoxConstraints.tightFor(width: 450), + child: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: l10n.selectLanguage, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, ), - // MenuSectionDescriptionWidget( - // content: context.l10n.maxDeviceLimitSpikeHandling(50), - // ) - ], - ), - ); - }, - childCount: 1, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: ItemsWidget( + supportedLocales, + onLocaleChanged, + currentLocale, + ), + ), + // MenuSectionDescriptionWidget( + // content: context.l10n.maxDeviceLimitSpikeHandling(50), + // ) + ], + ), + ); + }, + childCount: 1, + ), + ), + const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), + ], ), - const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), - ], + ), ), ); } @@ -79,8 +84,8 @@ class ItemsWidget extends StatefulWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ItemsWidgetState(); diff --git a/auth/lib/ui/settings/made_with_love_widget.dart b/auth/lib/ui/settings/made_with_love_widget.dart index ff1715efd..a8e12c106 100644 --- a/auth/lib/ui/settings/made_with_love_widget.dart +++ b/auth/lib/ui/settings/made_with_love_widget.dart @@ -4,8 +4,8 @@ import 'package:url_launcher/url_launcher.dart'; class MadeWithLoveWidget extends StatelessWidget { const MadeWithLoveWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/security_section_widget.dart b/auth/lib/ui/settings/security_section_widget.dart index f4cc3324f..4e50d38fb 100644 --- a/auth/lib/ui/settings/security_section_widget.dart +++ b/auth/lib/ui/settings/security_section_widget.dart @@ -16,15 +16,16 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/components/toggle_switch_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class SecuritySectionWidget extends StatefulWidget { - const SecuritySectionWidget({Key? key}) : super(key: key); + const SecuritySectionWidget({super.key}); @override State createState() => _SecuritySectionWidgetState(); @@ -94,6 +95,7 @@ class _SecuritySectionWidgetState extends State { ); final isEmailMFAEnabled = UserService.instance.hasEmailMFAEnabled(); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { await updateEmailMFA(!isEmailMFAEnabled); if (mounted) { @@ -117,6 +119,7 @@ class _SecuritySectionWidgetState extends State { context, context.l10n.authToViewYourActiveSessions, ); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { // ignore: unawaited_futures Navigator.of(context).push( @@ -149,6 +152,7 @@ class _SecuritySectionWidgetState extends State { context.l10n.lockScreenEnablePreSteps, ); if (hasAuthenticated) { + FocusScope.of(context).requestFocus(); setState(() {}); } }, diff --git a/auth/lib/ui/settings/settings_section_title.dart b/auth/lib/ui/settings/settings_section_title.dart index 08dbc69ec..8b7e549cf 100644 --- a/auth/lib/ui/settings/settings_section_title.dart +++ b/auth/lib/ui/settings/settings_section_title.dart @@ -8,9 +8,9 @@ class SettingsSectionTitle extends StatelessWidget { const SettingsSectionTitle( this.title, { - Key? key, + super.key, this.color, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/settings_text_item.dart b/auth/lib/ui/settings/settings_text_item.dart index 6273c7cad..afaf56432 100644 --- a/auth/lib/ui/settings/settings_text_item.dart +++ b/auth/lib/ui/settings/settings_text_item.dart @@ -8,10 +8,10 @@ class SettingsTextItem extends StatelessWidget { final String text; final IconData icon; const SettingsTextItem({ - Key? key, + super.key, required this.text, required this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/social_section_widget.dart b/auth/lib/ui/settings/social_section_widget.dart index 62b200453..cda18366c 100644 --- a/auth/lib/ui/settings/social_section_widget.dart +++ b/auth/lib/ui/settings/social_section_widget.dart @@ -7,11 +7,12 @@ import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SocialSectionWidget extends StatelessWidget { - const SocialSectionWidget({Key? key}) : super(key: key); + const SocialSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -31,8 +32,10 @@ class SocialSectionWidget extends StatelessWidget { final List options = [ sectionOptionSpacing, - SocialsMenuItemWidget(l10n.rateUsOnStore(ratePlace), rateUrl), - sectionOptionSpacing, + if (PlatformUtil.isMobile()) ...[ + SocialsMenuItemWidget(l10n.rateUsOnStore(ratePlace), rateUrl), + sectionOptionSpacing, + ], SocialsMenuItemWidget( l10n.blog, "https://ente.io/blog", @@ -65,11 +68,11 @@ class SocialsMenuItemWidget extends StatelessWidget { final bool launchInExternalApp; const SocialsMenuItemWidget( - this.text, - this.url, { - Key? key, - this.launchInExternalApp = true, - }) : super(key: key); + this.text, + this.url, { + super.key, + this.launchInExternalApp = true, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/support_dev_widget.dart b/auth/lib/ui/settings/support_dev_widget.dart index 63fa92c27..849b95415 100644 --- a/auth/lib/ui/settings/support_dev_widget.dart +++ b/auth/lib/ui/settings/support_dev_widget.dart @@ -10,8 +10,8 @@ import 'package:url_launcher/url_launcher.dart'; class SupportDevWidget extends StatelessWidget { const SupportDevWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/support_section_widget.dart b/auth/lib/ui/settings/support_section_widget.dart index 418c1d430..1343d2347 100644 --- a/auth/lib/ui/settings/support_section_widget.dart +++ b/auth/lib/ui/settings/support_section_widget.dart @@ -12,7 +12,7 @@ import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SupportSectionWidget extends StatefulWidget { - const SupportSectionWidget({Key? key}) : super(key: key); + const SupportSectionWidget({super.key}); @override State createState() => _SupportSectionWidgetState(); @@ -64,7 +64,7 @@ class _SupportSectionWidgetState extends State { onTap: () async { // ignore: unawaited_futures launchUrlString( - githubDiscussionsUrl, + githubIssuesUrl, mode: LaunchMode.externalApplication, ); }, diff --git a/auth/lib/ui/settings/theme_switch_widget.dart b/auth/lib/ui/settings/theme_switch_widget.dart index 69e4513f1..c0495b65b 100644 --- a/auth/lib/ui/settings/theme_switch_widget.dart +++ b/auth/lib/ui/settings/theme_switch_widget.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class ThemeSwitchWidget extends StatefulWidget { - const ThemeSwitchWidget({Key? key}) : super(key: key); + const ThemeSwitchWidget({super.key}); @override State createState() => _ThemeSwitchWidgetState(); diff --git a/auth/lib/ui/settings/title_bar_widget.dart b/auth/lib/ui/settings/title_bar_widget.dart index e11a5c287..8d9aff85c 100644 --- a/auth/lib/ui/settings/title_bar_widget.dart +++ b/auth/lib/ui/settings/title_bar_widget.dart @@ -2,7 +2,12 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:flutter/material.dart'; class SettingsTitleBarWidget extends StatelessWidget { - const SettingsTitleBarWidget({Key? key}) : super(key: key); + const SettingsTitleBarWidget({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; @override Widget build(BuildContext context) { @@ -17,7 +22,7 @@ class SettingsTitleBarWidget extends StatelessWidget { IconButton( visualDensity: const VisualDensity(horizontal: -2, vertical: -2), onPressed: () { - Navigator.pop(context); + scaffoldKey.currentState?.closeDrawer(); }, icon: const Icon(Icons.keyboard_double_arrow_left_outlined), ), diff --git a/auth/lib/ui/settings_page.dart b/auth/lib/ui/settings_page.dart index cfe5ba874..48fd6467c 100644 --- a/auth/lib/ui/settings_page.dart +++ b/auth/lib/ui/settings_page.dart @@ -26,18 +26,24 @@ import 'package:ente_auth/ui/settings/theme_switch_widget.dart'; import 'package:ente_auth/ui/settings/title_bar_widget.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; + final GlobalKey scaffoldKey; - SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); + SettingsPage({ + super.key, + required this.emailNotifier, + required this.scaffoldKey, + }); @override Widget build(BuildContext context) { - final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); - if (_hasLoggedIn) { + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + if (hasLoggedIn) { UserService.instance.getUserDetailsV2().ignore(); } final enteColorScheme = getEnteColorScheme(context); @@ -50,11 +56,11 @@ class SettingsPage extends StatelessWidget { } Widget _getBody(BuildContext context, EnteColorScheme colorScheme) { - final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); final enteTextTheme = getEnteTextTheme(context); const sectionSpacing = SizedBox(height: 8); final List contents = []; - if (_hasLoggedIn) { + if (hasLoggedIn) { contents.add( Container( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -111,6 +117,7 @@ class SettingsPage extends StatelessWidget { context, context.l10n.authToInitiateSignIn, ); + await PlatformUtil.refocusWindows(); if (!hasAuthenticated) { return; } @@ -163,7 +170,9 @@ class SettingsPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SettingsTitleBarWidget(), + SettingsTitleBarWidget( + scaffoldKey: scaffoldKey, + ), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Column( diff --git a/auth/lib/ui/tools/app_lock.dart b/auth/lib/ui/tools/app_lock.dart index 2e7e0f54b..df55f8116 100644 --- a/auth/lib/ui/tools/app_lock.dart +++ b/auth/lib/ui/tools/app_lock.dart @@ -37,7 +37,7 @@ class AppLock extends StatefulWidget { final Locale locale; const AppLock({ - Key? key, + super.key, required this.builder, required this.lockScreen, required this.savedThemeMode, @@ -46,7 +46,7 @@ class AppLock extends StatefulWidget { this.backgroundLockLatency = const Duration(seconds: 0), this.darkTheme, this.lightTheme, - }) : super(key: key); + }); static _AppLockState? of(BuildContext context) => context.findAncestorStateOfType<_AppLockState>(); @@ -135,9 +135,9 @@ class _AppLockState extends State with WidgetsBindingObserver { } Widget get _lockScreen { - return WillPopScope( + return PopScope( child: this.widget.lockScreen, - onWillPop: () => Future.value(false), + canPop: false, ); } diff --git a/auth/lib/ui/tools/debug/log_file_viewer.dart b/auth/lib/ui/tools/debug/log_file_viewer.dart index cc9321aa4..bcee33bc2 100644 --- a/auth/lib/ui/tools/debug/log_file_viewer.dart +++ b/auth/lib/ui/tools/debug/log_file_viewer.dart @@ -1,12 +1,12 @@ import 'dart:io'; -import 'dart:ui'; import 'package:ente_auth/ui/common/loading_widget.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; class LogFileViewer extends StatefulWidget { final File file; - const LogFileViewer(this.file, {Key? key}) : super(key: key); + const LogFileViewer(this.file, {super.key}); @override State createState() => _LogFileViewerState(); @@ -42,13 +42,17 @@ class _LogFileViewerState extends State { return Container( padding: const EdgeInsets.only(left: 12, top: 8, right: 12), child: SingleChildScrollView( - child: Text( - _logs!, - style: const TextStyle( - fontFeatures: [ - FontFeature.tabularFigures(), - ], - height: 1.2, + child: SelectableRegion( + focusNode: FocusNode(), + selectionControls: PlatformUtil.selectionControls, + child: Text( + _logs!, + style: const TextStyle( + fontFeatures: [ + FontFeature.tabularFigures(), + ], + height: 1.2, + ), ), ), ), diff --git a/auth/lib/ui/tools/lock_screen.dart b/auth/lib/ui/tools/lock_screen.dart index c2e725f1e..b6e2126e1 100644 --- a/auth/lib/ui/tools/lock_screen.dart +++ b/auth/lib/ui/tools/lock_screen.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class LockScreen extends StatefulWidget { - const LockScreen({Key? key}) : super(key: key); + const LockScreen({super.key}); @override State createState() => _LockScreenState(); diff --git a/auth/lib/ui/two_factor_authentication_page.dart b/auth/lib/ui/two_factor_authentication_page.dart index 43b8f967e..58a462286 100644 --- a/auth/lib/ui/two_factor_authentication_page.dart +++ b/auth/lib/ui/two_factor_authentication_page.dart @@ -4,13 +4,13 @@ import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/lifecycle_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pinput/pin_put/pin_put.dart'; + +import 'package:pinput/pinput.dart'; class TwoFactorAuthenticationPage extends StatefulWidget { final String sessionID; - const TwoFactorAuthenticationPage(this.sessionID, {Key? key}) - : super(key: key); + const TwoFactorAuthenticationPage(this.sessionID, {super.key}); @override State createState() => @@ -86,29 +86,31 @@ class _TwoFactorAuthenticationPageState const Padding(padding: EdgeInsets.all(32)), Padding( padding: const EdgeInsets.fromLTRB(40, 0, 40, 0), - child: PinPut( - fieldsCount: 6, - onSubmit: (String code) { + child: Pinput( + onSubmitted: (String code) { _verifyTwoFactorCode(code); }, + length: 6, + defaultPinTheme: const PinTheme(), + submittedPinTheme: PinTheme( + decoration: pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(20.0), + ), + ), + focusedPinTheme: PinTheme( + decoration: pinPutDecoration, + ), + followingPinTheme: PinTheme( + decoration: pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(5.0), + ), + ), onChanged: (String pin) { setState(() { _code = pin; }); }, controller: _pinController, - submittedFieldDecoration: pinPutDecoration.copyWith( - borderRadius: BorderRadius.circular(20.0), - ), - selectedFieldDecoration: pinPutDecoration, - followingFieldDecoration: pinPutDecoration.copyWith( - borderRadius: BorderRadius.circular(5.0), - ), - inputDecoration: const InputDecoration( - focusedBorder: InputBorder.none, - border: InputBorder.none, - counterText: '', - ), autofocus: true, ), ), diff --git a/auth/lib/ui/two_factor_recovery_page.dart b/auth/lib/ui/two_factor_recovery_page.dart index 4eae7ff2d..5743f0246 100644 --- a/auth/lib/ui/two_factor_recovery_page.dart +++ b/auth/lib/ui/two_factor_recovery_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/services/user_service.dart'; @@ -17,8 +15,8 @@ class TwoFactorRecoveryPage extends StatefulWidget { this.sessionID, this.encryptedSecret, this.secretDecryptionNonce, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _TwoFactorRecoveryPageState(); diff --git a/auth/lib/ui/utils/icon_utils.dart b/auth/lib/ui/utils/icon_utils.dart index 298a14333..771303952 100644 --- a/auth/lib/ui/utils/icon_utils.dart +++ b/auth/lib/ui/utils/icon_utils.dart @@ -87,7 +87,7 @@ class IconUtils { Color? _getAdaptiveColor(String? hexColor, BuildContext context) { if (hexColor == null) return null; final theme = Theme.of(context).brightness; - final color = Color(int.parse("0xFF" + hexColor)); + final color = Color(int.parse("0xFF$hexColor")); // Color is close to neutral-grey and it's too light or dark for theme if (_isCloseToNeutralGrey(color) && ((theme == Brightness.light && _getColorLuminance(color) > 0.70) || diff --git a/auth/lib/utils/auth_util.dart b/auth/lib/utils/auth_util.dart index 0a6320191..c2d2f5afa 100644 --- a/auth/lib/utils/auth_util.dart +++ b/auth/lib/utils/auth_util.dart @@ -1,38 +1,45 @@ +import 'dart:io'; + import 'package:ente_auth/l10n/l10n.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; import 'package:local_auth/local_auth.dart'; import 'package:local_auth_android/local_auth_android.dart'; -import 'package:local_auth_ios/types/auth_messages_ios.dart'; +import 'package:local_auth_darwin/types/auth_messages_ios.dart'; import 'package:logging/logging.dart'; Future requestAuthentication(BuildContext context, String reason) async { Logger("AuthUtil").info("Requesting authentication"); - await LocalAuthentication().stopAuthentication(); - final l10n = context.l10n; - return await LocalAuthentication().authenticate( - localizedReason: reason, - authMessages: [ - AndroidAuthMessages( - biometricHint: l10n.androidBiometricHint, - biometricNotRecognized: l10n.androidBiometricNotRecognized, - biometricRequiredTitle: l10n.androidBiometricRequiredTitle, - biometricSuccess: l10n.androidBiometricSuccess, - cancelButton: l10n.androidCancelButton, - deviceCredentialsRequiredTitle: - l10n.androidDeviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription: - l10n.androidDeviceCredentialsSetupDescription, - goToSettingsButton: l10n.goToSettings, - goToSettingsDescription: l10n.androidGoToSettingsDescription, - signInTitle: l10n.androidSignInTitle, - ), - IOSAuthMessages( - goToSettingsButton: l10n.goToSettings, - goToSettingsDescription: l10n.goToSettings, - lockOut: l10n.iOSLockOut, - // cancelButton default value is "Ok" - cancelButton: l10n.iOSOkButton, - ), - ], - ); + if (Platform.isMacOS || Platform.isLinux) { + return await FlutterLocalAuthentication().authenticate(); + } else { + await LocalAuthentication().stopAuthentication(); + final l10n = context.l10n; + return await LocalAuthentication().authenticate( + localizedReason: reason, + authMessages: [ + AndroidAuthMessages( + biometricHint: l10n.androidBiometricHint, + biometricNotRecognized: l10n.androidBiometricNotRecognized, + biometricRequiredTitle: l10n.androidBiometricRequiredTitle, + biometricSuccess: l10n.androidBiometricSuccess, + cancelButton: l10n.androidCancelButton, + deviceCredentialsRequiredTitle: + l10n.androidDeviceCredentialsRequiredTitle, + deviceCredentialsSetupDescription: + l10n.androidDeviceCredentialsSetupDescription, + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.androidGoToSettingsDescription, + signInTitle: l10n.androidSignInTitle, + ), + IOSAuthMessages( + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.goToSettings, + lockOut: l10n.iOSLockOut, + // cancelButton default value is "Ok" + cancelButton: l10n.iOSOkButton, + ), + ], + ); + } } diff --git a/auth/lib/utils/crypto_util.dart b/auth/lib/utils/crypto_util.dart deleted file mode 100644 index 494ef130f..000000000 --- a/auth/lib/utils/crypto_util.dart +++ /dev/null @@ -1,509 +0,0 @@ -import 'dart:convert'; -import 'dart:io' as io; -import 'dart:typed_data'; - -import 'package:computer/computer.dart'; -import 'package:ente_auth/core/errors.dart'; -import 'package:ente_auth/models/derived_key_result.dart'; -import 'package:ente_auth/models/encryption_result.dart'; -import 'package:ente_auth/utils/device_info.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:logging/logging.dart'; - -const int encryptionChunkSize = 4 * 1024 * 1024; -final int decryptionChunkSize = - encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes; -const int hashChunkSize = 4 * 1024 * 1024; -const int loginSubKeyLen = 32; -const int loginSubKeyId = 1; -const String loginSubKeyContext = "loginctx"; - -Uint8List cryptoSecretboxEasy(Map args) { - return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]); -} - -Uint8List cryptoSecretboxOpenEasy(Map args) { - return Sodium.cryptoSecretboxOpenEasy( - args["cipher"], - args["nonce"], - args["key"], - ); -} - -Uint8List cryptoPwHash(Map args) { - return Sodium.cryptoPwhash( - Sodium.cryptoSecretboxKeybytes, - args["password"], - args["salt"], - args["opsLimit"], - args["memLimit"], - Sodium.cryptoPwhashAlgArgon2id13, - ); -} - -Uint8List cryptoKdfDeriveFromKey( - Map args, -) { - return Sodium.cryptoKdfDeriveFromKey( - args["subkeyLen"], - args["subkeyId"], - args["context"], - args["key"], - ); -} - -// Returns the hash for a given file, chunking it in batches of hashChunkSize -Future cryptoGenericHash(Map args) async { - final sourceFile = io.File(args["sourceFilePath"]); - final sourceFileLength = await sourceFile.length(); - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final state = - Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax); - var bytesRead = 0; - bool isDone = false; - while (!isDone) { - var chunkSize = hashChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - isDone = true; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - Sodium.cryptoGenerichashUpdate(state, buffer); - } - await inputFile.close(); - return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax); -} - -EncryptionResult chachaEncryptData(Map args) { - final initPushResult = - Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]); - final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push( - initPushResult.state, - args["source"], - null, - Sodium.cryptoSecretstreamXchacha20poly1305TagFinal, - ); - return EncryptionResult( - encryptedData: encryptedData, - header: initPushResult.header, - ); -} - -// Encrypts a given file, in chunks of encryptionChunkSize -Future chachaEncryptFile(Map args) async { - final encryptionStartTime = DateTime.now().millisecondsSinceEpoch; - final logger = Logger("ChaChaEncrypt"); - final sourceFile = io.File(args["sourceFilePath"]); - final destinationFile = io.File(args["destinationFilePath"]); - final sourceFileLength = await sourceFile.length(); - logger.info("Encrypting file of size " + sourceFileLength.toString()); - - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen(); - final initPushResult = - Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key); - var bytesRead = 0; - var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage; - while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) { - var chunkSize = encryptionChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push( - initPushResult.state, - buffer, - null, - tag, - ); - await destinationFile.writeAsBytes(encryptedData, mode: io.FileMode.append); - } - await inputFile.close(); - - logger.info( - "Encryption time: " + - (DateTime.now().millisecondsSinceEpoch - encryptionStartTime) - .toString(), - ); - - return EncryptionResult(key: key, header: initPushResult.header); -} - -Future chachaDecryptFile(Map args) async { - final logger = Logger("ChaChaDecrypt"); - final decryptionStartTime = DateTime.now().millisecondsSinceEpoch; - final sourceFile = io.File(args["sourceFilePath"]); - final destinationFile = io.File(args["destinationFilePath"]); - final sourceFileLength = await sourceFile.length(); - logger.info("Decrypting file of size " + sourceFileLength.toString()); - - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull( - args["header"], - args["key"], - ); - - var bytesRead = 0; - var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage; - while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) { - var chunkSize = decryptionChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - final pullResult = - Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null); - await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append); - tag = pullResult.tag; - } - inputFile.closeSync(); - - logger.info( - "ChaCha20 Decryption time: " + - (DateTime.now().millisecondsSinceEpoch - decryptionStartTime) - .toString(), - ); -} - -Uint8List chachaDecryptData(Map args) { - final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull( - args["header"], - args["key"], - ); - final pullResult = Sodium.cryptoSecretstreamXchacha20poly1305Pull( - pullState, - args["source"], - null, - ); - return pullResult.m; -} - -class CryptoUtil { - // Note: workers are turned on during app startup. - static final Computer _computer = Computer.shared(); - - static init() { - Sodium.init(); - } - - static Uint8List base642bin( - String b64, { - String? ignore, - int variant = Sodium.base64VariantOriginal, - }) { - return Sodium.base642bin(b64, ignore: ignore, variant: variant); - } - - static String bin2base64( - Uint8List bin, { - bool urlSafe = false, - }) { - return Sodium.bin2base64( - bin, - variant: - urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal, - ); - } - - static String bin2hex(Uint8List bin) { - return Sodium.bin2hex(bin); - } - - static Uint8List hex2bin(String hex) { - return Sodium.hex2bin(hex); - } - - // Encrypts the given source, with the given key and a randomly generated - // nonce, using XSalsa20 (w Poly1305 MAC). - // This function runs on the same thread as the caller, so should be used only - // for small amounts of data where thread switching can result in a degraded - // user experience - static EncryptionResult encryptSync(Uint8List source, Uint8List key) { - final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes); - - final args = {}; - args["source"] = source; - args["nonce"] = nonce; - args["key"] = key; - final encryptedData = cryptoSecretboxEasy(args); - return EncryptionResult( - key: key, - nonce: nonce, - encryptedData: encryptedData, - ); - } - - // Decrypts the given cipher, with the given key and nonce using XSalsa20 - // (w Poly1305 MAC). - static Future decrypt( - Uint8List cipher, - Uint8List key, - Uint8List nonce, - ) async { - final args = {}; - args["cipher"] = cipher; - args["nonce"] = nonce; - args["key"] = key; - return _computer.compute( - cryptoSecretboxOpenEasy, - param: args, - taskName: "decrypt", - ); - } - - // Decrypts the given cipher, with the given key and nonce using XSalsa20 - // (w Poly1305 MAC). - // This function runs on the same thread as the caller, so should be used only - // for small amounts of data where thread switching can result in a degraded - // user experience - static Uint8List decryptSync( - Uint8List cipher, - Uint8List key, - Uint8List nonce, - ) { - final args = {}; - args["cipher"] = cipher; - args["nonce"] = nonce; - args["key"] = key; - return cryptoSecretboxOpenEasy(args); - } - - // Encrypts the given source, with the given key and a randomly generated - // nonce, using XChaCha20 (w Poly1305 MAC). - // This function runs on the isolate pool held by `_computer`. - // TODO: Remove "ChaCha", an implementation detail from the function name - static Future encryptChaCha( - Uint8List source, - Uint8List key, - ) async { - final args = {}; - args["source"] = source; - args["key"] = key; - return _computer.compute( - chachaEncryptData, - param: args, - taskName: "encryptChaCha", - ); - } - - // Decrypts the given source, with the given key and header using XChaCha20 - // (w Poly1305 MAC). - // TODO: Remove "ChaCha", an implementation detail from the function name - static Future decryptChaCha( - Uint8List source, - Uint8List key, - Uint8List header, - ) async { - final args = {}; - args["source"] = source; - args["key"] = key; - args["header"] = header; - return _computer.compute( - chachaDecryptData, - param: args, - taskName: "decryptChaCha", - ); - } - - // Encrypts the file at sourceFilePath, with the key (if provided) and a - // randomly generated nonce using XChaCha20 (w Poly1305 MAC), and writes it - // to the destinationFilePath. - // If a key is not provided, one is generated and returned. - static Future encryptFile( - String sourceFilePath, - String destinationFilePath, { - Uint8List? key, - }) { - final args = {}; - args["sourceFilePath"] = sourceFilePath; - args["destinationFilePath"] = destinationFilePath; - args["key"] = key; - return _computer.compute( - chachaEncryptFile, - param: args, - taskName: "encryptFile", - ); - } - - // Decrypts the file at sourceFilePath, with the given key and header using - // XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath. - static Future decryptFile( - String sourceFilePath, - String destinationFilePath, - Uint8List header, - Uint8List key, - ) { - final args = {}; - args["sourceFilePath"] = sourceFilePath; - args["destinationFilePath"] = destinationFilePath; - args["header"] = header; - args["key"] = key; - return _computer.compute( - chachaDecryptFile, - param: args, - taskName: "decryptFile", - ); - } - - // Generates and returns a 256-bit key. - static Uint8List generateKey() { - return Sodium.cryptoSecretboxKeygen(); - } - - // Generates and returns a random byte buffer of length - // crypto_pwhash_SALTBYTES (16) - static Uint8List getSaltToDeriveKey() { - return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes); - } - - // Generates and returns a secret key and the corresponding public key. - static Future generateKeyPair() async { - return Sodium.cryptoBoxKeypair(); - } - - // Decrypts the input using the given publicKey-secretKey pair - static Uint8List openSealSync( - Uint8List input, - Uint8List publicKey, - Uint8List secretKey, - ) { - return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey); - } - - // Encrypts the input using the given publicKey - static Uint8List sealSync(Uint8List input, Uint8List publicKey) { - return Sodium.cryptoBoxSeal(input, publicKey); - } - - // Derives a key for a given password and salt using Argon2id, v1.3. - // The function first attempts to derive a key with both memLimit and opsLimit - // set to their Sensitive variants. - // If this fails, say on a device with insufficient RAM, we retry by halving - // the memLimit and doubling the opsLimit, while ensuring that we stay within - // the min and max limits for both parameters. - // At all points, we ensure that the product of these two variables (the area - // under the graph that determines the amount of work required) is a constant. - static Future deriveSensitiveKey( - Uint8List password, - Uint8List salt, - ) async { - final logger = Logger("pwhash"); - int memLimit = Sodium.cryptoPwhashMemlimitSensitive; - int opsLimit = Sodium.cryptoPwhashOpslimitSensitive; - if (await isLowSpecDevice()) { - logger.info("low spec device detected"); - // When sensitive memLimit (1 GB) is used, on low spec device the OS might - // kill the app with OOM. To avoid that, start with 256 MB and - // corresponding ops limit (16). - // This ensures that the product of these two variables - // (the area under the graph that determines the amount of work required) - // stays the same - // SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE: 1073741824 - // SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE: 268435456 - // SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE: 4 - memLimit = Sodium.cryptoPwhashMemlimitModerate; - final factor = Sodium.cryptoPwhashMemlimitSensitive ~/ - Sodium.cryptoPwhashMemlimitModerate; // = 4 - opsLimit = opsLimit * factor; // = 16 - } - Uint8List key; - while (memLimit >= Sodium.cryptoPwhashMemlimitMin && - opsLimit <= Sodium.cryptoPwhashOpslimitMax) { - try { - key = await deriveKey(password, salt, memLimit, opsLimit); - return DerivedKeyResult(key, memLimit, opsLimit); - } catch (e, s) { - logger.warning( - "failed to deriveKey mem: $memLimit, ops: $opsLimit", - e, - s, - ); - } - memLimit = (memLimit / 2).round(); - opsLimit = opsLimit * 2; - } - throw UnsupportedError("Cannot perform this operation on this device"); - } - - // Derives a key for the given password and salt, using Argon2id, v1.3 - // with memory and ops limit hardcoded to their Interactive variants - // NOTE: This is only used while setting passwords for shared links, as an - // extra layer of authentication (atop the access token and collection key). - // More details @ https://ente.io/blog/building-shareable-links/ - static Future deriveInteractiveKey( - Uint8List password, - Uint8List salt, - ) async { - final int memLimit = Sodium.cryptoPwhashMemlimitInteractive; - final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive; - final key = await deriveKey(password, salt, memLimit, opsLimit); - return DerivedKeyResult(key, memLimit, opsLimit); - } - - // Derives a key for a given password, salt, memLimit and opsLimit using - // Argon2id, v1.3. - static Future deriveKey( - Uint8List password, - Uint8List salt, - int memLimit, - int opsLimit, - ) async { - try { - return await _computer.compute( - cryptoPwHash, - param: { - "password": password, - "salt": salt, - "memLimit": memLimit, - "opsLimit": opsLimit, - }, - taskName: "deriveKey", - ); - } catch (e, s) { - final String errMessage = 'failed to deriveKey memLimit: $memLimit and ' - 'opsLimit: $opsLimit'; - Logger("CryptoUtilDeriveKey").warning(errMessage, e, s); - throw KeyDerivationError(); - } - } - - // derives a Login key as subKey from the given key by applying KDF - // (Key Derivation Function) with the `loginSubKeyId` and - // `loginSubKeyLen` and `loginSubKeyContext` as context - static Future deriveLoginKey( - Uint8List key, - ) async { - try { - final Uint8List derivedKey = await _computer.compute( - cryptoKdfDeriveFromKey, - param: { - "key": key, - "subkeyId": loginSubKeyId, - "subkeyLen": loginSubKeyLen, - "context": utf8.encode(loginSubKeyContext), - }, - taskName: "deriveLoginKey", - ); - // return the first 16 bytes of the derived key - return derivedKey.sublist(0, 16); - } catch (e, s) { - Logger("deriveLoginKey").severe("loginKeyDerivation failed", e, s); - throw LoginKeyDerivationError(); - } - } - - // Computes and returns the hash of the source file - static Future getHash(io.File source) { - return _computer.compute( - cryptoGenericHash, - param: { - "sourceFilePath": source.path, - }, - taskName: "fileHash", - ); - } -} diff --git a/auth/lib/utils/data_util.dart b/auth/lib/utils/data_util.dart deleted file mode 100644 index 3dcae58fa..000000000 --- a/auth/lib/utils/data_util.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:math'; - -double convertBytesToGBs(final int bytes, {int precision = 2}) { - return double.parse( - (bytes / (1024 * 1024 * 1024)).toStringAsFixed(precision), - ); -} - -final storageUnits = ["bytes", "KB", "MB", "GB"]; - -String convertBytesToReadableFormat(int bytes) { - int storageUnitIndex = 0; - while (bytes >= 1024 && storageUnitIndex < storageUnits.length - 1) { - storageUnitIndex++; - bytes = (bytes / 1024).round(); - } - return bytes.toString() + " " + storageUnits[storageUnitIndex]; -} - -String formatBytes(int bytes, [int decimals = 2]) { - if (bytes == 0) return '0 bytes'; - const k = 1024; - final int dm = decimals < 0 ? 0 : decimals; - final int i = (log(bytes) / log(k)).floor(); - return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + storageUnits[i]; -} diff --git a/auth/lib/utils/date_time_util.dart b/auth/lib/utils/date_time_util.dart index 5ce25de2d..cd8099923 100644 --- a/auth/lib/utils/date_time_util.dart +++ b/auth/lib/utils/date_time_util.dart @@ -48,25 +48,17 @@ const searchStartYear = 1970; //Jun 2022 String getMonthAndYear(DateTime dateTime) { - return _months[dateTime.month]! + " " + dateTime.year.toString(); + return "${_months[dateTime.month]!} ${dateTime.year}"; } //Thu, 30 Jun String getDayAndMonth(DateTime dateTime) { - return _days[dateTime.weekday]! + - ", " + - dateTime.day.toString() + - " " + - _months[dateTime.month]!; + return "${_days[dateTime.weekday]!}, ${dateTime.day} ${_months[dateTime.month]!}"; } //30 Jun, 2022 String getDateAndMonthAndYear(DateTime dateTime) { - return dateTime.day.toString() + - " " + - _months[dateTime.month]! + - ", " + - dateTime.year.toString(); + return "${dateTime.day} ${_months[dateTime.month]!}, ${dateTime.year}"; } String getDay(DateTime dateTime) { @@ -87,13 +79,11 @@ String getAbbreviationOfYear(DateTime dateTime) { //14:32 String getTime(DateTime dateTime) { - final hours = dateTime.hour > 9 - ? dateTime.hour.toString() - : "0" + dateTime.hour.toString(); - final minutes = dateTime.minute > 9 - ? dateTime.minute.toString() - : "0" + dateTime.minute.toString(); - return hours + ":" + minutes; + final hours = + dateTime.hour > 9 ? dateTime.hour.toString() : "0${dateTime.hour}"; + final minutes = + dateTime.minute > 9 ? dateTime.minute.toString() : "0${dateTime.minute}"; + return "$hours:$minutes"; } //11:22 AM @@ -103,41 +93,23 @@ String getTimeIn12hrFormat(DateTime dateTime) { //Thu, Jun 30, 2022 - 14:32 String getFormattedTime(DateTime dateTime) { - return getDay(dateTime) + - ", " + - getMonth(dateTime) + - " " + - dateTime.day.toString() + - ", " + - dateTime.year.toString() + - " - " + - getTime(dateTime); + return "${getDay(dateTime)}, ${getMonth(dateTime)} ${dateTime.day}, ${dateTime.year} - ${getTime(dateTime)}"; } //30 Jun'22 String getFormattedDate(DateTime dateTime) { - return dateTime.day.toString() + - " " + - getMonth(dateTime) + - "'" + - getAbbreviationOfYear(dateTime); + return "${dateTime.day} ${getMonth(dateTime)}'${getAbbreviationOfYear(dateTime)}"; } String getFullDate(DateTime dateTime) { - return getDay(dateTime) + - ", " + - getMonth(dateTime) + - " " + - dateTime.day.toString() + - " " + - dateTime.year.toString(); + return "${getDay(dateTime)}, ${getMonth(dateTime)} ${dateTime.day} ${dateTime.year}"; } String daysLeft(int futureTime) { final int daysLeft = ((futureTime - DateTime.now().microsecondsSinceEpoch) / Duration.microsecondsPerDay) .ceil(); - return '$daysLeft day' + (daysLeft <= 1 ? "" : "s"); + return '$daysLeft day${daysLeft <= 1 ? "" : "s"}'; } String formatDuration(Duration position) { @@ -168,7 +140,7 @@ String formatDuration(Duration position) { : '0$seconds'; final formattedTime = - '${hoursString == '00' ? '' : hoursString + ':'}$minutesString:$secondsString'; + '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; return formattedTime; } @@ -223,7 +195,7 @@ String getDayTitle(int timestamp) { } } if (date.year != DateTime.now().year) { - title += " " + date.year.toString(); + title += " ${date.year}"; } return title; } @@ -233,14 +205,11 @@ String secondsToHHMMSS(int value) { h = value ~/ 3600; m = ((value - h * 3600)) ~/ 60; s = value - (h * 3600) - (m * 60); - final String hourLeft = - h.toString().length < 2 ? "0" + h.toString() : h.toString(); + final String hourLeft = h.toString().length < 2 ? "0$h" : h.toString(); - final String minuteLeft = - m.toString().length < 2 ? "0" + m.toString() : m.toString(); + final String minuteLeft = m.toString().length < 2 ? "0$m" : m.toString(); - final String secondsLeft = - s.toString().length < 2 ? "0" + s.toString() : s.toString(); + final String secondsLeft = s.toString().length < 2 ? "0$s" : s.toString(); final String result = "$hourLeft:$minuteLeft:$secondsLeft"; diff --git a/auth/lib/utils/device_info.dart b/auth/lib/utils/device_info.dart deleted file mode 100644 index 5832ec6de..000000000 --- a/auth/lib/utils/device_info.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; - -late DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - -// https://gist.github.com/adamawolf/3048717 -late Set iOSLowEndMachineCodes = { - "iPhone5,1", //iPhone 5 (GSM) - "iPhone5,2", //iPhone 5 (GSM+CDMA) - "iPhone5,3", //iPhone 5C (GSM) - "iPhone5,4", //iPhone 5C (Global) - "iPhone6,1", //iPhone 5S (GSM) - "iPhone6,2", //iPhone 5S (Global) - "iPhone7,1", //iPhone 6 Plus - "iPhone7,2", //iPhone 6 - "iPhone8,1", // iPhone 6s - "iPhone8,2", // iPhone 6s Plus - "iPhone8,4", // iPhone SE (GSM) - "iPhone9,1", // iPhone 7 - "iPhone9,2", // iPhone 7 Plus - "iPhone9,3", // iPhone 7 - "iPhone9,4", // iPhone 7 Plus - "iPhone10,1", // iPhone 8 - "iPhone10,2", // iPhone 8 Plus - "iPhone10,3", // iPhone X Global - "iPhone10,4", // iPhone 8 - "iPhone10,5", // iPhone 8 -}; - -Future isLowSpecDevice() async { - try { - if (Platform.isIOS) { - final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; - debugPrint("ios utc name ${iosInfo.utsname.machine}"); - return iOSLowEndMachineCodes.contains(iosInfo.utsname.machine); - } - } catch (e) { - Logger("device_info").severe("deviceSpec check failed", e); - } - return false; -} diff --git a/auth/lib/utils/dialog_util.dart b/auth/lib/utils/dialog_util.dart index 9cf7273fd..d24608b78 100644 --- a/auth/lib/utils/dialog_util.dart +++ b/auth/lib/utils/dialog_util.dart @@ -47,11 +47,11 @@ Future showErrorDialogForException({ String apiErrorPrefix = "It looks like something went wrong.", }) async { String errorMessage = context.l10n.tempErrorContactSupportIfPersists; - if (exception is DioError && + if (exception is DioException && exception.response != null && exception.response!.data["code"] != null) { errorMessage = - "$apiErrorPrefix\n\nReason: " + exception.response!.data["code"]; + "$apiErrorPrefix\n\nReason: ${exception.response!.data["code"]}"; } return showDialogWidget( context: context, diff --git a/auth/lib/utils/directory_utils.dart b/auth/lib/utils/directory_utils.dart new file mode 100644 index 000000000..b9da6e613 --- /dev/null +++ b/auth/lib/utils/directory_utils.dart @@ -0,0 +1,12 @@ +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class DirectoryUtils { + static Future getDatabasePath(String databaseName) async => p.joinAll( + [ + (await getApplicationDocumentsDirectory()).path, + "ente", + ".$databaseName", + ], + ); +} diff --git a/auth/lib/utils/email_util.dart b/auth/lib/utils/email_util.dart index d276962b1..582449edb 100644 --- a/auth/lib/utils/email_util.dart +++ b/auth/lib/utils/email_util.dart @@ -4,22 +4,21 @@ import 'package:archive/archive_io.dart'; import 'package:email_validator/email_validator.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/logging/super_logging.dart'; -import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/tools/debug/log_file_viewer.dart'; -// import 'package:ente_auth/ui/tools/debug/log_file_viewer.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; -import 'package:file_saver/file_saver.dart'; +import "package:file_saver/file_saver.dart"; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; -import 'package:intl/intl.dart'; +import "package:intl/intl.dart"; import 'package:logging/logging.dart'; -// import 'package:open_mail_app/open_mail_app.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -27,7 +26,10 @@ import 'package:url_launcher/url_launcher.dart'; final Logger _logger = Logger('email_util'); -bool isValidEmail(String email) { +bool isValidEmail(String? email) { + if (email == null) { + return false; + } return EmailValidator.validate(email); } @@ -40,92 +42,73 @@ Future sendLogs( String? body, }) async { final l10n = context.l10n; - final List actions = [ - TextButton( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - Icons.feed_outlined, - color: Theme.of(context).iconTheme.color?.withOpacity(0.85), - ), - const Padding(padding: EdgeInsets.all(4)), - Text( - l10n.viewLogsAction, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .defaultTextColor - .withOpacity(0.85), + await showDialogWidget( + context: context, + title: title, + icon: Icons.bug_report_outlined, + body: l10n.sendLogsDescription, + buttons: [ + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.neutral, + labelText: l10n.reportABug, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + onTap: () async { + await _sendLogs(context, toEmail, subject, body); + if (postShare != null) { + postShare(); + } + }, + ), + //isInAlert is false here as we don't want to the dialog to dismiss + //on pressing this button + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: l10n.viewLogsAction, + buttonAction: ButtonAction.second, + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) { + return LogFileViewer(SuperLogging.logFile!); + }, + barrierColor: Colors.black87, + barrierDismissible: false, + ); + }, + ), + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.exportLogs, + buttonAction: ButtonAction.third, + onTap: () async { + Future.delayed( + const Duration(milliseconds: 200), + () => shareDialog( + context, + title, + saveAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath); + }, + sendAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath, true); + }, ), - ), - ], + ); + }, ), - onPressed: () async { - // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return LogFileViewer(SuperLogging.logFile!); - }, - barrierColor: Colors.black87, - barrierDismissible: false, - ); - }, - ), - TextButton( - child: Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.alternativeColor, - ), - ), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop('dialog'); - await _sendLogs(context, toEmail, subject, body); - if (postShare != null) { - postShare(); - } - }, - ), - ]; - final List content = []; - content.addAll( - [ - Text( - l10n.sendLogsDescription, - style: const TextStyle( - height: 1.5, - fontSize: 16, - ), - ), - const Padding(padding: EdgeInsets.all(12)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: actions, + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.cancel, + buttonAction: ButtonAction.cancel, ), ], ); - final confirmation = AlertDialog( - title: Text( - title, - style: const TextStyle( - fontSize: 18, - ), - ), - content: SingleChildScrollView( - child: ListBody( - children: content, - ), - ), - ); - // ignore: unawaited_futures - showDialog( - context: context, - builder: (_) { - return confirmation; - }, - ); } Future _sendLogs( @@ -146,6 +129,7 @@ Future _sendLogs( await FlutterEmailSender.send(email); } catch (e, s) { _logger.severe('email sender failed', e, s); + Navigator.of(context, rootNavigator: true).pop(); await shareLogs(context, toEmail, zipFilePath); } } @@ -155,10 +139,10 @@ Future getZippedLogsFile(BuildContext context) async { final dialog = createProgressDialog(context, l10n.preparingLogsTitle); await dialog.show(); final logsPath = (await getApplicationSupportDirectory()).path; - final logsDirectory = Directory(logsPath + "/logs"); + final logsDirectory = Directory("$logsPath/logs"); final tempPath = (await getTemporaryDirectory()).path; final zipFilePath = - tempPath + "/logs-${Configuration.instance.getUserID() ?? 0}.zip"; + "$tempPath/logs-${Configuration.instance.getUserID() ?? 0}.zip"; final encoder = ZipFileEncoder(); encoder.create(zipFilePath); await encoder.addDirectory(logsDirectory); @@ -172,14 +156,15 @@ Future shareLogs( String toEmail, String zipFilePath, ) async { + final l10n = context.l10n; final result = await showDialogWidget( context: context, - title: context.l10n.emailYourLogs, - body: context.l10n.pleaseSendTheLogsTo(toEmail), + title: l10n.emailYourLogs, + body: l10n.pleaseSendTheLogsTo(toEmail), buttons: [ ButtonWidget( buttonType: ButtonType.neutral, - labelText: context.l10n.copyEmailAddress, + labelText: l10n.copyEmailAddress, isInAlert: true, buttonAction: ButtonAction.first, onTap: () async { @@ -189,35 +174,55 @@ Future shareLogs( ), ButtonWidget( buttonType: ButtonType.neutral, - labelText: context.l10n.exportLogs, + labelText: l10n.exportLogs, isInAlert: true, buttonAction: ButtonAction.second, ), ButtonWidget( buttonType: ButtonType.secondary, - labelText: context.l10n.cancel, + labelText: l10n.cancel, isInAlert: true, buttonAction: ButtonAction.cancel, ), ], ); if (result?.action != null && result!.action == ButtonAction.second) { - await exportLogs(context, zipFilePath); + Future.delayed( + const Duration(milliseconds: 200), + () => shareDialog( + context, + context.l10n.exportLogs, + saveAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath); + }, + sendAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath, true); + }, + ), + ); } } -Future exportLogs(BuildContext context, String zipFilePath) async { +Future exportLogs( + BuildContext context, + String zipFilePath, [ + bool isSharing = false, +]) async { final Size size = MediaQuery.of(context).size; - if (Platform.isAndroid) { - DateTime now = DateTime.now().toUtc(); - String shortMonthName = DateFormat('MMM').format(now); // Short month name - String logFileName = + if (!isSharing) { + final DateTime now = DateTime.now().toUtc(); + final String shortMonthName = DateFormat('MMM').format(now); // Short month + final String logFileName = 'ente-logs-${now.year}-$shortMonthName-${now.day}-${now.hour}-${now.minute}'; - await FileSaver.instance.saveAs( - name: logFileName, - filePath: zipFilePath, - mimeType: MimeType.zip, - ext: 'zip', + + final bytes = await File(zipFilePath).readAsBytes(); + await PlatformUtil.shareFile( + logFileName, + 'zip', + bytes, + MimeType.zip, ); } else { await Share.shareXFiles( @@ -235,8 +240,8 @@ Future sendEmail( }) async { try { final String clientDebugInfo = await _clientInfo(); - final String _subject = subject ?? '[Support]'; - final String _body = (body ?? '') + clientDebugInfo; + final String subject0 = subject ?? '[Support]'; + final String body0 = (body ?? '') + clientDebugInfo; // final EmailContent email = EmailContent( // to: [ // to, @@ -250,7 +255,7 @@ Future sendEmail( final Uri params = Uri( scheme: 'mailto', path: to, - query: 'subject=$_subject&body=$_body', + query: 'subject=$subject0&body=$body0', ); if (await canLaunchUrl(params)) { await launchUrl(params); @@ -280,27 +285,15 @@ Future _clientInfo() async { void _showNoMailAppsDialog(BuildContext context, String toEmail) { final l10n = context.l10n; - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(l10n.emailUsMessage(toEmail)), - actions: [ - TextButton( - child: Text(l10n.copyEmailAction), - onPressed: () async { - await Clipboard.setData(ClipboardData(text: toEmail)); - showShortToast(context, l10n.copied); - }, - ), - TextButton( - child: Text(l10n.ok), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ); + showChoiceDialog( + context, + icon: Icons.email_outlined, + title: l10n.emailUsMessage(toEmail), + firstButtonLabel: l10n.copyEmailAddress, + secondButtonLabel: l10n.ok, + firstButtonOnTap: () async { + await Clipboard.setData(ClipboardData(text: toEmail)); + showShortToast(context, l10n.copied); }, ); } diff --git a/auth/lib/utils/navigation_util.dart b/auth/lib/utils/navigation_util.dart index 38a3dbe53..58aa0fb85 100644 --- a/auth/lib/utils/navigation_util.dart +++ b/auth/lib/utils/navigation_util.dart @@ -109,9 +109,9 @@ class SwipeableRouteBuilder extends PageRoute { class TransparentRoute extends PageRoute { TransparentRoute({ required this.builder, - RouteSettings? settings, + super.settings, }) : assert(builder != null), - super(settings: settings, fullscreenDialog: false); + super(fullscreenDialog: false); final WidgetBuilder? builder; diff --git a/auth/lib/utils/package_info_util.dart b/auth/lib/utils/package_info_util.dart new file mode 100644 index 000000000..0bb5c2458 --- /dev/null +++ b/auth/lib/utils/package_info_util.dart @@ -0,0 +1,20 @@ +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class PackageInfoUtil { + Future getPackageInfo() async { + return await PackageInfo.fromPlatform(); + } + + String getVersion(PackageInfo info) { + return info.version; + } + + String getPackageName(PackageInfo info) { + if (PlatformUtil.isMobile()) { + return info.packageName; + } else { + return 'io.ente.auth'; + } + } +} diff --git a/auth/lib/utils/platform_util.dart b/auth/lib/utils/platform_util.dart new file mode 100644 index 000000000..5e0270257 --- /dev/null +++ b/auth/lib/utils/platform_util.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class PlatformUtil { + static bool isDesktop() { + return !kIsWeb && + (Platform.isWindows || Platform.isLinux || Platform.isMacOS); + } + + static bool isMobile() { + return !kIsWeb && (Platform.isAndroid || Platform.isIOS); + } + + static bool isWeb() { + return kIsWeb; + } + + static TextSelectionControls get selectionControls => Platform.isAndroid + ? materialTextSelectionControls + : Platform.isIOS + ? cupertinoTextSelectionControls + : desktopTextSelectionControls; + + static openWebView(BuildContext context, String title, String url) async { + if (PlatformUtil.isDesktop()) { + if (!await WebviewWindow.isWebviewAvailable()) { + await launchUrlString(url); + return; + } + + final webview = await WebviewWindow.create( + configuration: CreateConfiguration( + title: title, + ), + ); + webview.launch(url); + return; + } + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return WebPage( + title, + url, + ); + }, + ), + ); + } + + static Future shareFile( + String fileName, + String extension, + Uint8List bytes, + MimeType type, + ) async { + try { + if (Platform.isAndroid || Platform.isIOS) { + await FileSaver.instance.saveAs( + name: fileName, + ext: extension, + bytes: bytes, + mimeType: type, + ); + } else { + await FileSaver.instance.saveFile( + name: fileName, + ext: extension, + bytes: bytes, + mimeType: type, + ); + } + } catch (_) {} + } + + // Needed to fix issue with local_auth on Windows + // https://github.com/flutter/flutter/issues/122322 + static Future refocusWindows() async { + if (!Platform.isWindows) return; + await windowManager.blur(); + await windowManager.focus(); + await windowManager.setAlwaysOnTop(true); + await windowManager.setAlwaysOnTop(false); + } +} diff --git a/auth/lib/utils/share_utils.dart b/auth/lib/utils/share_utils.dart new file mode 100644 index 000000000..5bb8e0848 --- /dev/null +++ b/auth/lib/utils/share_utils.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +import 'package:ente_auth/ui/components/dialog_widget.dart'; +import 'package:ente_auth/ui/components/models/button_type.dart'; +import 'package:flutter/material.dart'; + +Future shareDialog( + BuildContext context, + String title, { + required Function saveAction, + required Function sendAction, +}) async { + final l10n = context.l10n; + await showDialogWidget( + context: context, + title: title, + body: Platform.isLinux || Platform.isWindows + ? l10n.saveOnlyDescription + : l10n.saveOrSendDescription, + buttons: [ + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.neutral, + labelText: l10n.save, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + onTap: () async { + await saveAction(); + }, + ), + if (!Platform.isWindows && !Platform.isLinux) + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.send, + buttonAction: ButtonAction.second, + onTap: () async { + await sendAction(); + }, + ), + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.cancel, + buttonAction: ButtonAction.cancel, + ), + ], + ); +} diff --git a/auth/lib/utils/toast_util.dart b/auth/lib/utils/toast_util.dart index c275160eb..942274bc8 100644 --- a/auth/lib/utils/toast_util.dart +++ b/auth/lib/utils/toast_util.dart @@ -1,5 +1,6 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; void showToast( @@ -8,16 +9,42 @@ void showToast( toastLength = Toast.LENGTH_LONG, iOSDismissOnTap = true, }) async { - await Fluttertoast.cancel(); - await Fluttertoast.showToast( - msg: message, - toastLength: toastLength, - gravity: ToastGravity.BOTTOM, - timeInSecForIosWeb: 1, - backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor, - textColor: Theme.of(context).colorScheme.toastTextColor, - fontSize: 16.0, - ); + try { + await Fluttertoast.cancel(); + await Fluttertoast.showToast( + msg: message, + toastLength: toastLength, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor, + textColor: Theme.of(context).colorScheme.toastTextColor, + fontSize: 16.0, + ); + } on MissingPluginException catch (_) { + Widget toast = Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Theme.of(context).colorScheme.toastBackgroundColor, + ), + child: Text( + message, + style: TextStyle( + color: Theme.of(context).colorScheme.toastTextColor, + fontSize: 16.0, + ), + ), + ); + + final fToast = FToast(); + fToast.init(context); + + fToast.showToast( + child: toast, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + } } void showShortToast(context, String message) { diff --git a/auth/lib/utils/window_protocol_handler.dart b/auth/lib/utils/window_protocol_handler.dart new file mode 100644 index 000000000..a1436ef01 --- /dev/null +++ b/auth/lib/utils/window_protocol_handler.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +const _hive = HKEY_CURRENT_USER; + +class WindowsProtocolHandler { + void register(String scheme, {String? executable, List? arguments}) { + final prefix = _regPrefix(scheme); + final capitalized = scheme[0].toUpperCase() + scheme.substring(1); + final cmd = executable ?? Platform.resolvedExecutable; + + _regCreateStringKey(_hive, prefix, '', 'URL:$capitalized'); + _regCreateStringKey(_hive, prefix, 'URL Protocol', ''); + _regCreateStringKey(_hive, '$prefix\\shell\\open\\command', '', cmd); + } + + void unregister(String scheme) { + final txtKey = TEXT(_regPrefix(scheme)); + try { + RegDeleteTree(HKEY_CURRENT_USER, txtKey); + } finally { + free(txtKey); + } + } + + String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme'; + + int _regCreateStringKey(int hKey, String key, String valueName, String data) { + final txtKey = TEXT(key); + final txtValue = TEXT(valueName); + final txtData = TEXT(data); + try { + return RegSetKeyValue( + hKey, + txtKey, + txtValue, + REG_SZ, + txtData, + txtData.length * 2 + 2, + ); + } finally { + free(txtKey); + free(txtValue); + free(txtData); + } + } + + // String _sanitize(String value) { + // value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"'); + // return '"$value"'; + // } +} diff --git a/auth/linux/CMakeLists.txt b/auth/linux/CMakeLists.txt index 68e736ee3..ca3bd0a60 100644 --- a/auth/linux/CMakeLists.txt +++ b/auth/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "authenticator") +set(BINARY_NAME "ente_auth") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "io.ente.auth") diff --git a/auth/linux/flutter/generated_plugin_registrant.cc b/auth/linux/flutter/generated_plugin_registrant.cc index 53de79447..bcc3b982d 100644 --- a/auth/linux/flutter/generated_plugin_registrant.cc +++ b/auth/linux/flutter/generated_plugin_registrant.cc @@ -6,22 +6,58 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include +#include +#include #include +#include +#include +#include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) file_saver_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); file_saver_plugin_register_with_registrar(file_saver_registrar); + g_autoptr(FlPluginRegistrar) flutter_local_authentication_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalAuthenticationPlugin"); + flutter_local_authentication_plugin_register_with_registrar(flutter_local_authentication_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); + g_autoptr(FlPluginRegistrar) smart_auth_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin"); + smart_auth_plugin_register_with_registrar(smart_auth_registrar); + g_autoptr(FlPluginRegistrar) sodium_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SodiumLibsPlugin"); + sodium_libs_plugin_register_with_registrar(sodium_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/auth/linux/flutter/generated_plugins.cmake b/auth/linux/flutter/generated_plugins.cmake index 4b981d07a..c6aab9a95 100644 --- a/auth/linux/flutter/generated_plugins.cmake +++ b/auth/linux/flutter/generated_plugins.cmake @@ -3,10 +3,19 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window file_saver + flutter_local_authentication flutter_secure_storage_linux + gtk + screen_retriever sentry_flutter + smart_auth + sodium_libs + sqlite3_flutter_libs + tray_manager url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/auth/linux/my_application.cc b/auth/linux/my_application.cc index 95b3a369c..8a553fec3 100644 --- a/auth/linux/my_application.cc +++ b/auth/linux/my_application.cc @@ -7,17 +7,27 @@ #include "flutter/generated_plugin_registrant.h" -struct _MyApplication { +struct _MyApplication +{ GtkApplication parent_instance; - char** dart_entrypoint_arguments; + char **dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = +static void my_application_activate(GApplication *application) +{ + MyApplication *self = MY_APPLICATION(application); + + GList *windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) + { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + + GtkWindow *window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used @@ -29,31 +39,36 @@ static void my_application_activate(GApplication* application) { // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) + { + const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) + { use_header_bar = FALSE; } } #endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + if (use_header_bar) + { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "authenticator"); + gtk_header_bar_set_title(header_bar, "Ente Auth"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "authenticator"); + } + else + { + gtk_window_set_title(window, "Ente Auth"); } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_realize(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - FlView* view = fl_view_new(project); + FlView *view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); @@ -63,42 +78,47 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); +static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status) +{ + MyApplication *self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) + { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); +static void my_application_dispose(GObject *object) +{ + MyApplication *self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init(MyApplicationClass* klass) { +static void my_application_class_init(MyApplicationClass *klass) +{ G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init(MyApplication* self) {} +static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/auth/linux/packaging/appimage/make_config.yaml b/auth/linux/packaging/appimage/make_config.yaml new file mode 100644 index 000000000..4f4e1a4cd --- /dev/null +++ b/auth/linux/packaging/appimage/make_config.yaml @@ -0,0 +1,27 @@ +display_name: Auth +license: GPLv3 + +icon: assets/icons/auth-icon.png + +keywords: + - Authentication + - 2FA + +generic_name: Ente Auth + +categories: + - Utility + +startup_notify: false + +# You can specify the shared libraries that you want to bundle with your app +# +# flutter_distributor automatically detects the shared libraries that your app +# depends on, but you can also specify them manually here. +# +# The following example shows how to bundle the libcurl library with your app. +# +# include: +# - libcurl.so.4 +include: + - libffi.so.7 diff --git a/auth/linux/packaging/deb/make_config.yaml b/auth/linux/packaging/deb/make_config.yaml new file mode 100644 index 000000000..93f54e472 --- /dev/null +++ b/auth/linux/packaging/deb/make_config.yaml @@ -0,0 +1,34 @@ +display_name: Auth +package_name: auth +maintainer: + name: Ente.io Developers + email: human@ente.io +priority: optional +section: x11 +essential: false +license: GPLv3 +icon: assets/icons/auth-icon.png +installed_size: 36000 + +dependencies: + - libwebkit2gtk-4.0-37 + - libsqlite3-0 + - libsodium23 + - libsecret-1-0 + - libappindicator3-1 | libayatana-appindicator3-1 + - gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1 + - libayatana-ido3-0.4-0 + +keywords: + - Authentication + - 2FA + +generic_name: Ente Authentication + +categories: + - Utility + +startup_notify: false + +supported_mime_type: + - x-scheme-handler/ente diff --git a/auth/linux/packaging/rpm/make_config.yaml b/auth/linux/packaging/rpm/make_config.yaml new file mode 100644 index 000000000..2b1a6a7fe --- /dev/null +++ b/auth/linux/packaging/rpm/make_config.yaml @@ -0,0 +1,31 @@ +icon: assets/icons/auth-icon.png +summary: 2FA app with free end-to-end encrypted backup and sync +group: Application/Utility +vendor: Ente.io +packager: Ente.io Developers +packagerEmail: human@ente.io +license: GPLv3 +url: https://github.com/ente-io/ente + +display_name: Auth + +dependencies: + - libsqlite3x + - webkit2gtk-4.0 + - libsodium + - libsecret + - libappindicator + +keywords: + - Authentication + - 2FA + +generic_name: Ente Authentication + +categories: + - Utility + +startup_notify: false + +supported_mime_type: + - x-scheme-handler/ente diff --git a/auth/macos/Flutter/GeneratedPluginRegistrant.swift b/auth/macos/Flutter/GeneratedPluginRegistrant.swift index 6b18cc381..68c59c25a 100644 --- a/auth/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/auth/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,30 +5,50 @@ import FlutterMacOS import Foundation -import connectivity_macos +import app_links +import connectivity_plus +import desktop_webview_window import device_info_plus import file_saver +import flutter_inappwebview_macos +import flutter_local_authentication import flutter_local_notifications import flutter_secure_storage_macos import package_info_plus import path_provider_foundation +import screen_retriever import sentry_flutter import share_plus import shared_preferences_foundation +import smart_auth +import sodium_libs import sqflite +import sqlite3_flutter_libs +import tray_manager import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) + SodiumLibsPlugin.register(with: registry.registrar(forPlugin: "SodiumLibsPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/auth/macos/Podfile b/auth/macos/Podfile index 049abe295..0c308d4f5 100644 --- a/auth/macos/Podfile +++ b/auth/macos/Podfile @@ -36,5 +36,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) - end + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['export_xcargs'] = '-allowProvisioningUpdates' + end +end end diff --git a/auth/macos/Podfile.lock b/auth/macos/Podfile.lock index 678bce8da..a5b6eb77c 100644 --- a/auth/macos/Podfile.lock +++ b/auth/macos/Podfile.lock @@ -1,102 +1,186 @@ PODS: - - connectivity_macos (0.0.1): + - app_links (1.0.0): + - FlutterMacOS + - connectivity_plus (0.0.1): + - FlutterMacOS + - ReachabilitySwift + - desktop_webview_window (0.0.1): - FlutterMacOS - - Reachability - device_info_plus (0.0.1): - FlutterMacOS + - file_saver (0.0.1): + - FlutterMacOS + - flutter_inappwebview_macos (0.0.1): + - FlutterMacOS + - OrderedSet (~> 5.0) + - flutter_local_authentication (1.2.0): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - package_info_plus_macos (0.0.1): + - OrderedSet (5.0.0) + - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - Reachability (3.2) - - Sentry/HybridSDK (7.31.5) + - ReachabilitySwift (5.0.0) + - screen_retriever (0.0.1): + - FlutterMacOS + - Sentry/HybridSDK (8.21.0): + - SentryPrivate (= 8.21.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 7.31.5) - - share_plus_macos (0.0.1): + - Sentry/HybridSDK (= 8.21.0) + - SentryPrivate (8.21.0) + - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - smart_auth (0.0.1): + - FlutterMacOS + - sodium_libs (2.2.1): + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - sqlite3 (3.45.1): + - sqlite3/common (= 3.45.1) + - sqlite3/common (3.45.1) + - sqlite3/fts5 (3.45.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.45.1): + - sqlite3/common + - sqlite3/rtree (3.45.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.45.1) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - tray_manager (0.0.1): - FlutterMacOS - - FMDB (>= 2.7.5) - url_launcher_macos (0.0.1): - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS DEPENDENCIES: - - connectivity_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos`) + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) + - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) + - flutter_local_authentication (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`) + - sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: - - FMDB - - Reachability + - OrderedSet + - ReachabilitySwift - Sentry + - SentryPrivate + - sqlite3 EXTERNAL SOURCES: - connectivity_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + desktop_webview_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos + flutter_inappwebview_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos + flutter_local_authentication: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral - package_info_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + smart_auth: + :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos + sodium_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + tray_manager: + :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9 + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + file_saver: 44e6fbf666677faf097302460e214e977fdd977b + flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d + flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - Sentry: 4c9babff9034785067c896fd580b1f7de44da020 - sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + Sentry: ebc12276bd17613a114ab359074096b6b3725203 + sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e + SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 + sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 + sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386 + tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 -PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 +PODFILE CHECKSUM: f401c31c8f7c5571f6f565c78915d54338812dab -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/auth/macos/Runner.xcodeproj/project.pbxproj b/auth/macos/Runner.xcodeproj/project.pbxproj index cbd8b2c8a..492f710ad 100644 --- a/auth/macos/Runner.xcodeproj/project.pbxproj +++ b/auth/macos/Runner.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = authenticator.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Ente Auth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ente Auth.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -94,7 +94,6 @@ 4F2F733D93DB4D2D82767271 /* Pods-Runner.release.xcconfig */, B347CC163E4E13C897729F91 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -123,7 +122,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* authenticator.app */, + 33CC10ED2044A3C60003C045 /* Ente Auth.app */, ); name = Products; sourceTree = ""; @@ -193,7 +192,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* authenticator.app */; + productReference = 33CC10ED2044A3C60003C045 /* Ente Auth.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -203,13 +202,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; @@ -420,13 +418,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -546,13 +549,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -566,13 +574,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + OTHER_CODE_SIGN_FLAGS = "--timestamp"; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 38ba92a69..7b82ae2ae 100644 --- a/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/auth/macos/Runner/AppDelegate.swift b/auth/macos/Runner/AppDelegate.swift index d53ef6437..218f93e02 100644 --- a/auth/macos/Runner/AppDelegate.swift +++ b/auth/macos/Runner/AppDelegate.swift @@ -4,6 +4,6 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + return false } } diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png new file mode 100644 index 000000000..bbf24e436 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png new file mode 100644 index 000000000..2a210095a Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png new file mode 100644 index 000000000..fb83d3abe Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png new file mode 100644 index 000000000..f64b470b0 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png new file mode 100644 index 000000000..c72e503af Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png new file mode 100644 index 000000000..07f8c930f Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png new file mode 100644 index 000000000..d7c149e3d Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..eba1335b9 100644 --- a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7c..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc1642..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be6138..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a65286..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a72613..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2dd..000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/auth/macos/Runner/Configs/AppInfo.xcconfig b/auth/macos/Runner/Configs/AppInfo.xcconfig index 7c1cda27a..cb548680b 100644 --- a/auth/macos/Runner/Configs/AppInfo.xcconfig +++ b/auth/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,12 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = auth +PRODUCT_NAME = Ente Auth // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 io.ente.auth. All rights reserved. + +DEVELOPMENT_TEAM = 6Z68YJY9Q2 \ No newline at end of file diff --git a/auth/macos/Runner/DebugProfile.entitlements b/auth/macos/Runner/DebugProfile.entitlements index c946719a1..bc070d381 100644 --- a/auth/macos/Runner/DebugProfile.entitlements +++ b/auth/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,9 @@ com.apple.security.network.client + keychain-access-groups + + com.apple.security.files.downloads.read-write + diff --git a/auth/macos/Runner/Info.plist b/auth/macos/Runner/Info.plist index 4789daa6a..418d695c9 100644 --- a/auth/macos/Runner/Info.plist +++ b/auth/macos/Runner/Info.plist @@ -28,5 +28,20 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleURLSchemes + + otpauth + + + + LSApplicationCategoryType + public.app-category.security diff --git a/auth/macos/Runner/MainFlutterWindow.swift b/auth/macos/Runner/MainFlutterWindow.swift index 2722837ec..a823d046a 100644 --- a/auth/macos/Runner/MainFlutterWindow.swift +++ b/auth/macos/Runner/MainFlutterWindow.swift @@ -1,5 +1,6 @@ import Cocoa import FlutterMacOS +import window_manager class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -12,4 +13,9 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } + + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } } diff --git a/auth/macos/Runner/Release.entitlements b/auth/macos/Runner/Release.entitlements index 48271acc9..e04b5e06c 100644 --- a/auth/macos/Runner/Release.entitlements +++ b/auth/macos/Runner/Release.entitlements @@ -4,7 +4,11 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + com.apple.security.network.client - + + keychain-access-groups + diff --git a/auth/macos/build/.last_build_id b/auth/macos/build/.last_build_id new file mode 100644 index 000000000..637ab0382 --- /dev/null +++ b/auth/macos/build/.last_build_id @@ -0,0 +1 @@ +f51f5c3bcecb0339dc02189e9dd2c2c8 \ No newline at end of file diff --git a/auth/macos/packaging/dmg/make_config.yaml b/auth/macos/packaging/dmg/make_config.yaml new file mode 100644 index 000000000..30628bef3 --- /dev/null +++ b/auth/macos/packaging/dmg/make_config.yaml @@ -0,0 +1,11 @@ +title: Auth +icon: ../../../assets/icons/auth-icon.png +contents: + - x: 448 + y: 344 + type: link + path: "/Applications" + - x: 192 + y: 344 + type: file + path: Ente Auth.app diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 4514e503c..35dd96356 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -5,34 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" adaptive_theme: dependency: "direct main" description: name: adaptive_theme - sha256: "3568bb526d4823c7bb35f9ce3604af15e04cc0e9cc4f257da3604fe6b48d74ae" + sha256: f4ee609b464e5efc68131d9d15ba9aa1de4e3b5ede64be17781c6e19a52d637d url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.6.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + url: "https://pub.dev" + source: hosted + version: "3.5.1" archive: dependency: "direct main" description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.10" args: dependency: transitive description: @@ -69,18 +85,10 @@ packages: dependency: "direct main" description: name: bloc - sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.2" - bloc_test: - dependency: "direct dev" - description: - name: bloc_test - sha256: "43d5b2f3d09ba768d6b611151bdf20ca141ffb46e795eb9550a58c9c2f4eae3f" - url: "https://pub.dev" - source: hosted - version: "9.1.3" + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -93,10 +101,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -109,34 +117,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.7+1" + version: "7.3.0" built_collection: dependency: transitive description: @@ -149,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.1" characters: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clipboard: dependency: "direct main" description: @@ -189,27 +205,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" - computer: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "82e365fed8a1a76f6eea0220de98389eed7b0445" - url: "https://github.com/ente-io/computer.git" - source: git - version: "3.2.1" + version: "1.18.0" confetti: dependency: "direct main" description: @@ -218,62 +225,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - connectivity: + connectivity_plus: dependency: "direct main" description: - name: connectivity - sha256: a8e91263cf3e25fb5cc95e19dfde4999e32a648ac3b9e8a558a28165731678f8 + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "3.0.6" - connectivity_for_web: + version: "5.0.2" + connectivity_plus_platform_interface: dependency: transitive description: - name: connectivity_for_web - sha256: "01a390c1d5adc2ed1fa1f52d120c07fe9fd01166a93f965a832fd6cfc0ea6482" + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a url: "https://pub.dev" source: hosted - version: "0.4.0+1" - connectivity_macos: - dependency: transitive - description: - name: connectivity_macos - sha256: "51ae08d5162eca9669b9d8951ed83ce19c5355a81149f94e4dee2740beb93628" - url: "https://pub.dev" - source: hosted - version: "0.2.1+2" - connectivity_platform_interface: - dependency: transitive - description: - name: connectivity_platform_interface - sha256: "2d82e942df9d49f29a24bb07fb5ce085d4a53e47818c62364d2b6deb9e0d7a8e" - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "1.2.4" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" - url: "https://pub.dev" - source: hosted - version: "1.6.4" cross_file: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -290,38 +273,39 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" - source: hosted - version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" + desktop_webview_window: + dependency: "direct main" + description: + path: "packages/desktop_webview_window" + ref: HEAD + resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1" + url: "https://github.com/MixinNetwork/flutter-plugins" + source: git + version: "0.2.4" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -330,30 +314,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - diff_match_patch: - dependency: transitive - description: - name: diff_match_patch - sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" - url: "https://pub.dev" - source: hosted - version: "0.4.1" dio: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "0978e9a3e45305a80a7210dbeaf79d6ee8bee33f70c8e542dc654c952070217f" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.4.2+1" dotted_border: dependency: "direct main" description: name: dotted_border - sha256: "07a5c5e8d4e6e992279e190e0352be8faa5b8f96d81c77a78b2d42f060279840" + sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04" url: "https://pub.dev" source: hosted - version: "2.0.0+3" + version: "2.1.0" email_validator: dependency: "direct main" description: @@ -362,6 +338,23 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + ente_crypto_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: e2e66ffd03f23bef5e0bb138b5f01b32d8e9b7bb + url: "https://github.com/ente-io/ente_crypto_dart.git" + source: git + version: "1.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" event_bus: dependency: "direct main" description: @@ -395,39 +388,39 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: name: file_picker - sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.2.1" file_saver: dependency: "direct main" description: name: file_saver - sha256: "591d25e750e3a4b654f7b0293abc2ed857242f82ca7334051b2a8ceeb369dac8" + sha256: bdebc720e17b3e01aba59da69b6d47020a7e5ba7d5c75bd9194f9618d5f16ef4 url: "https://pub.dev" source: hosted - version: "0.2.8" + version: "0.2.12" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" @@ -451,10 +444,18 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.5" + flutter_context_menu: + dependency: "direct main" + description: + name: flutter_context_menu + sha256: "9f220a8fa0290c68e38000d6d62a0dc4555d490c15a5bd856a6e6d255d81b8dc" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_displaymode: dependency: "direct main" description: @@ -467,50 +468,107 @@ packages: dependency: "direct main" description: name: flutter_email_sender - sha256: "9e253c69617f43d4cb5de672e93a7a19c12a21fb6a75e66c6ce7626336c4c1bc" + sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "6.0.2" flutter_inappwebview: dependency: "direct main" description: name: flutter_inappwebview - sha256: f73505c792cf083d5566e1a94002311be497d984b5607f25be36d685cf6361cf + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" url: "https://pub.dev" source: hosted - version: "5.7.2+3" + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_launcher_icons: dependency: "direct main" description: name: flutter_launcher_icons - sha256: "559c600f056e7c704bd843723c21e01b5fba47e8824bd02422165bcc02a5de1d" + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.13.1" + flutter_local_authentication: + dependency: "direct main" + description: + path: "." + ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + resolved-ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + url: "https://github.com/eaceto/flutter_local_authentication" + source: git + version: "1.2.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "16.3.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "3c6d6db334f609a92be0c0915f40871ec56f5d2adf01e77ae364162c587c0ca8" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -520,99 +578,99 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.4.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.0.0" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" - url: "https://pub.dev" - source: hosted - version: "1.1.3" + path: flutter_secure_storage_linux + ref: patch-1 + resolved-ref: da8ab43bc51c8c3249a261c33b27aa6f018f819b + url: "https://github.com/prateekmedia/flutter_secure_storage.git" + source: git + version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: "6c68e1fad129b4b807b2218ef4cf7f7f6f61c5ec8861c990dc2278d9d03cb09f" + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" url: "https://pub.dev" source: hosted - version: "2.0.0" - flutter_sodium: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "267435eaf07af60b94406adf14bedf21e08a6b4f" - url: "https://github.com/ente-io/flutter_sodium.git" - source: git - version: "0.2.0" + version: "3.1.0" flutter_speed_dial: dependency: "direct main" description: name: flutter_speed_dial - sha256: "41d7ad0bc224248637b3a5e0b9083e912a75445bdb450cf82b8ed06d7af7c61d" + sha256: "698a037274a66dbae8697c265440e6acb6ab6cae9ac5f95c749e7944d8f28d41" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "7.0.0" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -627,18 +685,26 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "8.2.4" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -663,6 +729,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hex: dependency: transitive description: @@ -683,10 +757,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -707,10 +781,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.1.7" intl: dependency: "direct main" description: @@ -747,58 +821,82 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.6.2" + version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: "direct dev" description: name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "3.0.0" local_auth: dependency: "direct main" description: name: local_auth - sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115" + sha256: "280421b416b32de31405b0a25c3bd42dfcef2538dfbb20c03019e02a5ed55ed0" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.0" local_auth_android: dependency: "direct main" description: name: local_auth_android - sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5" + sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" url: "https://pub.dev" source: hosted - version: "1.0.31" - local_auth_ios: + version: "1.0.37" + local_auth_darwin: dependency: "direct main" description: - name: local_auth_ios - sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940 + name: local_auth_darwin + sha256: "33381a15b0de2279523eca694089393bb146baebdce72a404555d03174ebc1e9" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.2" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface - sha256: "9e160d59ef0743e35f1b50f4fb84dc64f55676b1b8071e319ef35e7f3bc13367" + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.0.10" local_auth_windows: dependency: transitive description: name: local_auth_windows - sha256: "19323b75ab781d5362dbb15dcb7e0916d2431c7a6dbdda016ec9708689877f73" + sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.10" logging: dependency: "direct main" description: @@ -811,50 +909,58 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mocktail: dependency: "direct dev" description: name: mocktail - sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.3" modal_bottom_sheet: dependency: "direct main" description: name: modal_bottom_sheet - sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f" + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e url: "https://pub.dev" source: hosted - version: "3.0.0-pre" + version: "3.0.0" move_to_background: dependency: "direct main" description: @@ -871,14 +977,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - node_preamble: + nm: dependency: transitive description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "0.5.0" otp: dependency: "direct main" description: @@ -899,10 +1005,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -920,13 +1026,13 @@ packages: source: hosted version: "0.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -947,90 +1053,90 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.0.2" pinput: dependency: "direct main" description: name: pinput - sha256: "27eb69042f75755bdb6544f6e79a50a6ed09d6e97e2d75c8421744df1e392949" + sha256: a92b55ecf9c25d1b9e100af45905385d5bc34fc9b6b04177a9e82cb88fe4d805 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "3.0.1" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.8" pointycastle: dependency: "direct main" description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1047,30 +1153,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.6" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" protobuf: dependency: "direct main" description: name: protobuf - sha256: "4034a02b7e231e7e60bff30a8ac13a7347abfdac0798595fae0b90a3f0afe759" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" provider: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1091,10 +1189,10 @@ packages: dependency: transitive description: name: qr - sha256: "5c4208b4dc0d55c3184d10d83ee0ded6212dc2b5e2ba17c5a0c0aab279128d21" + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.1" qr_code_scanner: dependency: "direct main" description: @@ -1107,98 +1205,106 @@ packages: dependency: "direct main" description: name: qr_flutter - sha256: c5c121c54cb6dd837b9b9d57eb7bc7ec6df4aee741032060c8833a678c80b87e + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.dev" + source: hosted + version: "0.1.9" sentry: dependency: "direct main" description: name: sentry - sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba" + sha256: a460aa48568d47140dd0557410b624d344ffb8c05555107ac65035c1097cf1ad url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.18.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: ff68ab31918690da004a42e20204242a3ad9ad57da7e2712da8487060ac9767f + sha256: "3d0d1d4e0e407d276ae8128d123263ccbc37e988bae906765efd6f37d544f4c6" url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.18.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shelf: dependency: transitive description: @@ -1207,22 +1313,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -1231,19 +1321,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + sodium: + dependency: transitive + description: + name: sodium + sha256: d9830a388e37c82891888e64cfd4c6764fa3ac716bed80ac6eab89ee42c3cd76 + url: "https://pub.dev" + source: hosted + version: "2.3.1+1" + sodium_libs: + dependency: transitive + description: + name: sodium_libs + sha256: f7f6719b7ab3e8512ce7a5ecd7bc8d865482431cdd5a07a46b55b13c152b54e1 + url: "https://pub.dev" + source: hosted + version: "2.2.1+1" source_gen: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1252,22 +1374,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" source_span: dependency: transitive description: @@ -1276,30 +1382,63 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: - name: sqflite - sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 - url: "https://pub.dev" - source: hosted - version: "2.2.8+4" + path: sqflite + ref: HEAD + resolved-ref: "075b3e2f81e691a19a500e7ff6db2953de7f83a9" + url: "https://github.com/tekartik/sqflite" + source: git + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.4.5+1" + version: "2.5.4" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + url: "https://pub.dev" + source: hosted + version: "0.5.20" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" step_progress_indicator: dependency: "direct main" description: @@ -1312,10 +1451,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1336,18 +1475,18 @@ packages: dependency: "direct main" description: name: styled_text - sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "8.1.0" synchronized: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1356,30 +1495,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - test: - dependency: transitive - description: - name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" - url: "https://pub.dev" - source: hosted - version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" - test_core: - dependency: transitive - description: - name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" - url: "https://pub.dev" - source: hosted - version: "0.5.3" + version: "0.6.1" timezone: dependency: transitive description: @@ -1396,6 +1519,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225" + url: "https://pub.dev" + source: hosted + version: "0.2.1" tuple: dependency: "direct main" description: @@ -1412,134 +1543,118 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uni_links: - dependency: "direct main" - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_platform_interface: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.0.36" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.3.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1552,74 +1667,82 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" + version: "2.4.4" win32: - dependency: transitive + dependency: "direct main" description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.3.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + url: "https://pub.dev" + source: hosted + version: "0.3.8" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" xmlstream: dependency: transitive description: name: xmlstream - sha256: "2d10c69a9d5fc46f71798b80ee6db15bc0d5bf560fdbdd264776cbeee0c83631" + sha256: cfc14e3f256997897df9481ae630d94c2d85ada5187ebeb868bb1aabc2c977b4 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" yaml: dependency: transitive description: @@ -1629,5 +1752,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 5e7808407..7f8e20bc8 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 2.0.42+242 +version: 2.0.50+250 publish_to: none environment: @@ -8,87 +8,112 @@ environment: dependencies: adaptive_theme: ^3.1.0 # done + app_links: ^3.5.0 archive: ^3.3.7 base32: ^2.1.3 bip39: ^1.0.6 #done - bloc: ^8.0.3 #done + bloc: ^8.1.2 clipboard: ^0.1.3 collection: # dart - computer: - git: "https://github.com/ente-io/computer.git" confetti: ^0.7.0 - connectivity: ^3.0.3 - cupertino_icons: ^1.0.0 - device_info_plus: ^8.0.0 - dio: ^4.0.6 + connectivity_plus: ^5.0.2 + convert: ^3.1.1 + desktop_webview_window: + git: + url: https://github.com/MixinNetwork/flutter-plugins + path: packages/desktop_webview_window + device_info_plus: ^9.1.1 + dio: ^5.4.0 dotted_border: ^2.0.0+2 email_validator: ^2.0.1 + ente_crypto_dart: + git: + url: https://github.com/ente-io/ente_crypto_dart.git event_bus: ^2.0.0 expandable: ^5.0.1 expansion_tile_card: ^3.0.0 - file_picker: ^5.2.4 + ffi: ^2.1.0 + file_picker: ^6.1.1 # https://github.com/incrediblezayed/file_saver/issues/86 - file_saver: 0.2.8 + file_saver: ^0.2.11 + fixnum: ^1.1.0 fk_user_agent: ^2.1.0 flutter: sdk: flutter flutter_bloc: ^8.0.1 + flutter_context_menu: ^0.1.3 flutter_displaymode: ^0.6.0 - flutter_email_sender: ^5.1.0 - flutter_inappwebview: ^5.7.1 - flutter_launcher_icons: ^0.9.3 - flutter_local_notifications: ^12.0.3 + flutter_email_sender: ^6.0.2 + flutter_inappwebview: ^6.0.0 + flutter_launcher_icons: ^0.13.1 + flutter_local_authentication: + git: + url: https://github.com/eaceto/flutter_local_authentication + ref: 1ac346a04592a05fd75acccf2e01fa3c7e955d96 + flutter_local_notifications: ^16.3.1+1 flutter_localizations: sdk: flutter flutter_native_splash: ^2.2.13 - flutter_secure_storage: ^8.0.0 - flutter_slidable: ^2.0.0 - flutter_sodium: - git: - url: https://github.com/ente-io/flutter_sodium.git - flutter_speed_dial: ^6.2.0 + flutter_secure_storage: ^9.0.0 + flutter_slidable: ^3.0.1 + flutter_speed_dial: ^7.0.0 + flutter_staggered_grid_view: ^0.7.0 flutter_svg: ^2.0.5 fluttertoast: ^8.1.1 google_nav_bar: ^5.0.5 #supported http: ^1.1.0 intl: ^0.18.0 json_annotation: ^4.5.0 - local_auth: ^2.1.7 - local_auth_android: ^1.0.31 - local_auth_ios: ^1.1.3 + local_auth: ^2.2.0 + local_auth_android: ^1.0.37 + local_auth_darwin: ^1.2.2 logging: ^1.0.1 modal_bottom_sheet: ^3.0.0-pre move_to_background: ^1.0.2 otp: ^3.1.1 package_info_plus: ^4.1.0 password_strength: ^0.2.0 + path: ^1.8.3 path_provider: ^2.0.11 - pinput: ^1.2.2 + pinput: ^3.0.1 pointycastle: ^3.7.3 privacy_screen: ^0.0.6 protobuf: ^3.0.0 qr_code_scanner: ^1.0.1 - qr_flutter: 4.0.0 + qr_flutter: ^4.1.0 sentry: ^7.9.0 sentry_flutter: ^7.9.0 share_plus: ^7.2.1 shared_preferences: ^2.0.5 - sqflite: ^2.1.0 + sqflite: + git: + url: https://github.com/tekartik/sqflite + path: sqflite + sqflite_common_ffi: ^2.3.0+4 + sqlite3: ^2.1.0 + sqlite3_flutter_libs: ^0.5.19+1 step_progress_indicator: ^1.0.2 - styled_text: ^7.0.0 + styled_text: ^8.1.0 + tray_manager: ^0.2.1 tuple: ^2.0.0 - uni_links: ^0.5.1 url_launcher: ^6.1.5 - uuid: ^3.0.4 + uuid: ^4.2.2 + win32: ^5.1.1 + window_manager: ^0.3.8 +dependency_overrides: + flutter_secure_storage_linux: + git: + url: https://github.com/prateekmedia/flutter_secure_storage.git + ref: patch-1 + path: flutter_secure_storage_linux dev_dependencies: - bloc_test: ^9.0.3 build_runner: ^2.1.11 flutter_test: sdk: flutter json_serializable: ^6.2.0 - lints: ^1.0.1 - mocktail: ^0.3.0 + lints: ^3.0.0 + mocktail: ^1.0.3 # The following section is specific to Flutter. flutter: @@ -98,6 +123,7 @@ flutter: # https://docs:flutter:dev/development/ui/assets-and-images: assets: - assets/ + - assets/icons/ - assets/simple-icons/icons/ - assets/simple-icons/_data/ - assets/custom-icons/icons/ @@ -117,10 +143,10 @@ flutter: flutter_icons: android: "launcher_icon" - adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png" + adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png" adaptive_icon_background: "#ffffff" ios: true - image_path: "assets/icon-light.png" + image_path: "assets/generation-icons/icon-light.png" remove_alpha_ios: true flutter_native_splash: diff --git a/auth/web/index.html b/auth/web/index.html index 998d81d85..ef953df53 100644 --- a/auth/web/index.html +++ b/auth/web/index.html @@ -21,13 +21,13 @@ - + - My App + Auth diff --git a/auth/web/manifest.json b/auth/web/manifest.json index 232359953..eaf5b0fab 100644 --- a/auth/web/manifest.json +++ b/auth/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "My App", - "short_name": "My App", + "name": "Auth", + "short_name": "Auth", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/auth/windows/CMakeLists.txt b/auth/windows/CMakeLists.txt index d481d1864..46c315972 100644 --- a/auth/windows/CMakeLists.txt +++ b/auth/windows/CMakeLists.txt @@ -1,13 +1,16 @@ +# Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(ente_auth LANGUAGES CXX) +project(auth LANGUAGES CXX) -set(BINARY_NAME "ente_auth") +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "auth") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -29,7 +32,11 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) -# Compilation ui.settings that should be applied to most targets. +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/auth/windows/flutter/CMakeLists.txt b/auth/windows/flutter/CMakeLists.txt index b2e4bd8d6..903f4899d 100644 --- a/auth/windows/flutter/CMakeLists.txt +++ b/auth/windows/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/auth/windows/flutter/generated_plugin_registrant.cc b/auth/windows/flutter/generated_plugin_registrant.cc index 20f612aa8..4184d5485 100644 --- a/auth/windows/flutter/generated_plugin_registrant.cc +++ b/auth/windows/flutter/generated_plugin_registrant.cc @@ -6,24 +6,54 @@ #include "generated_plugin_registrant.h" +#include +#include +#include #include +#include #include #include +#include #include #include +#include +#include +#include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FileSaverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSaverPlugin")); + FlutterLocalAuthenticationPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLocalAuthenticationPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + SmartAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SmartAuthPlugin")); + SodiumLibsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SodiumLibsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/auth/windows/flutter/generated_plugins.cmake b/auth/windows/flutter/generated_plugins.cmake index 41d3e5061..a2c679c88 100644 --- a/auth/windows/flutter/generated_plugins.cmake +++ b/auth/windows/flutter/generated_plugins.cmake @@ -3,12 +3,22 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links + connectivity_plus + desktop_webview_window file_saver + flutter_local_authentication flutter_secure_storage_windows local_auth_windows + screen_retriever sentry_flutter share_plus + smart_auth + sodium_libs + sqlite3_flutter_libs + tray_manager url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/auth/windows/packaging/exe/inno_setup.iss b/auth/windows/packaging/exe/inno_setup.iss new file mode 100644 index 000000000..5906ecbd0 --- /dev/null +++ b/auth/windows/packaging/exe/inno_setup.iss @@ -0,0 +1,64 @@ +[Setup] +AppId={{APP_ID}} +AppVersion={{APP_VERSION}} +AppName={{DISPLAY_NAME}} +AppPublisher={{PUBLISHER_NAME}} +AppPublisherURL={{PUBLISHER_URL}} +AppSupportURL={{PUBLISHER_URL}} +AppUpdatesURL={{PUBLISHER_URL}} +DefaultDirName={{INSTALL_DIR_NAME}} +DisableProgramGroupPage=yes +OutputDir=. +OutputBaseFilename={{OUTPUT_BASE_FILENAME}} +Compression=lzma +SolidCompression=yes +SetupIconFile={{SETUP_ICON_FILE}} +WizardStyle=modern +;PrivilegesRequired={{PRIVILEGES_REQUIRED}} +PrivilegesRequiredOverridesAllowed=dialog +ArchitecturesAllowed=x64 +ArchitecturesInstallIn64BitMode=x64 +UninstallDisplayIcon={app}\auth.exe + +[Languages] +{% for locale in LOCALES %} +{% if locale == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} +{% if locale == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %} +{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %} +{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %} +{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %} +{% if locale == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %} +{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %} +{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %} +{% if locale == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %} +{% if locale == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %} +{% if locale == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %} +{% if locale == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %} +{% if locale == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %} +{% if locale == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %} +{% if locale == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %} +{% if locale == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %} +{% if locale == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %} +{% if locale == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %} +{% if locale == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %} +{% if locale == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %} +{% if locale == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %} +{% if locale == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %} +{% if locale == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %} +{% if locale == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %} +{% if locale == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %} +{% endfor %} + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} +Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %} +[Files] +Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" +Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon +Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup +[Run] +Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent diff --git a/auth/windows/packaging/exe/make_config.yaml b/auth/windows/packaging/exe/make_config.yaml new file mode 100644 index 000000000..378d2d4d8 --- /dev/null +++ b/auth/windows/packaging/exe/make_config.yaml @@ -0,0 +1,10 @@ +app_id: 9E5F0C93-96A3-4DA9-AE52-1AA6339851FC +publisher: ente.io +publisher_url: https://github.com/ente-io/ente +display_name: Ente Auth +create_desktop_icon: false +install_dir_name: "{autopf}\\Ente Auth" +setup_icon_file: ../../assets/icons/auth-icon.ico +script_template: inno_setup.iss +locales: + - en diff --git a/auth/windows/runner/CMakeLists.txt b/auth/windows/runner/CMakeLists.txt index de2d8916b..394917c05 100644 --- a/auth/windows/runner/CMakeLists.txt +++ b/auth/windows/runner/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" @@ -10,8 +15,26 @@ add_executable(${BINARY_NAME} WIN32 "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/auth/windows/runner/Runner.rc b/auth/windows/runner/Runner.rc index 6c586f270..398fdf68b 100644 --- a/auth/windows/runner/Runner.rc +++ b/auth/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "Ente Technologies, Inc." "\0" - VALUE "FileDescription", "ente Authenticator" "\0" + VALUE "FileDescription", "Ente Auth" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "ente Authenticator" "\0" - VALUE "LegalCopyright", "Copyright (C) 2022 Ente Technologies, Inc.. All rights reserved." "\0" - VALUE "OriginalFilename", "ente_auth.exe" "\0" - VALUE "ProductName", "ente Authenticator" "\0" + VALUE "InternalName", "Ente Auth" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 Ente Technologies, Inc.. All rights reserved." "\0" + VALUE "OriginalFilename", "auth.exe" "\0" + VALUE "ProductName", "Ente Auth" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/auth/windows/runner/flutter_window.cpp b/auth/windows/runner/flutter_window.cpp index b43b9095e..1672f9cbf 100644 --- a/auth/windows/runner/flutter_window.cpp +++ b/auth/windows/runner/flutter_window.cpp @@ -26,6 +26,17 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + // [window_manager] + // this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } diff --git a/auth/windows/runner/main.cpp b/auth/windows/runner/main.cpp index df82ae0a5..11751936c 100644 --- a/auth/windows/runner/main.cpp +++ b/auth/windows/runner/main.cpp @@ -5,11 +5,58 @@ #include "flutter_window.h" #include "utils.h" +// [app_links] +#include "app_links/app_links_plugin_c_api.h" +bool SendAppLinkToInstance(const std::wstring &title) +{ + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) + { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; + GetWindowPlacement(hwnd, &place); + + switch (place.showCmd) + { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ + // [app_links] + if (SendAppLinkToInstance(L"Ente Auth")) + { + return EXIT_SUCCESS; + } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -27,13 +74,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"My App", origin, size)) { + if (!window.Create(L"Ente Auth", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } diff --git a/auth/windows/runner/resource.h b/auth/windows/runner/resource.h index d7b448fc5..66a65d1e4 100644 --- a/auth/windows/runner/resource.h +++ b/auth/windows/runner/resource.h @@ -1,4 +1,4 @@ -// +//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // diff --git a/auth/windows/runner/resources/app_icon.ico b/auth/windows/runner/resources/app_icon.ico index c04e20caf..38bb22bcf 100644 Binary files a/auth/windows/runner/resources/app_icon.ico and b/auth/windows/runner/resources/app_icon.ico differ diff --git a/auth/windows/runner/runner.exe.manifest b/auth/windows/runner/runner.exe.manifest index c977c4a42..a42ea7687 100644 --- a/auth/windows/runner/runner.exe.manifest +++ b/auth/windows/runner/runner.exe.manifest @@ -7,7 +7,7 @@ - + diff --git a/auth/windows/runner/utils.cpp b/auth/windows/runner/utils.cpp index d19bdbbcc..b2b08734d 100644 --- a/auth/windows/runner/utils.cpp +++ b/auth/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/auth/windows/runner/win32_window.cpp b/auth/windows/runner/win32_window.cpp index c10f08dc7..d3e9d1c60 100644 --- a/auth/windows/runner/win32_window.cpp +++ b/auth/windows/runner/win32_window.cpp @@ -1,50 +1,76 @@ #include "win32_window.h" +#include #include #include "resource.h" -namespace { +namespace +{ -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + /// Registry key for app theme preference. + /// + /// A value of 0 indicates apps should use dark mode. A non-zero or missing + /// value indicates apps should use light mode. + constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} + // The number of Win32Window objects that currently exist. + static int g_active_window_count = 0; -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + + // Scale helper to convert logical scaler values to physical using passed in + // scale factor + int Scale(int source, double scale_factor) + { + return static_cast(source * scale_factor); } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); + + // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. + // This API is only needed for PerMonitor V1 awareness mode. + void EnableFullDpiSupportIfAvailable(HWND hwnd) + { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) + { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) + { + enable_non_client_dpi_scaling(hwnd); + } FreeLibrary(user32_module); } -} -} // namespace +} // namespace // Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: +class WindowClassRegistrar +{ +public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { + // Returns the singleton registrar instance. + static WindowClassRegistrar *GetInstance() + { + if (!instance_) + { instance_ = new WindowClassRegistrar(); } return instance_; @@ -52,24 +78,26 @@ class WindowClassRegistrar { // Returns the name of the window class, registering the class if it hasn't // previously been registered. - const wchar_t* GetWindowClass(); + const wchar_t *GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); - private: +private: WindowClassRegistrar() = default; - static WindowClassRegistrar* instance_; + static WindowClassRegistrar *instance_; bool class_registered_ = false; }; -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; +WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { +const wchar_t *WindowClassRegistrar::GetWindowClass() +{ + if (!class_registered_) + { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; @@ -88,26 +116,30 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { return kWindowClassName; } -void WindowClassRegistrar::UnregisterWindowClass() { +void WindowClassRegistrar::UnregisterWindowClass() +{ UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } -Win32Window::Win32Window() { +Win32Window::Win32Window() +{ ++g_active_window_count; } -Win32Window::~Win32Window() { +Win32Window::~Win32Window() +{ --g_active_window_count; Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring &title, + const Point &origin, + const Size &size) +{ Destroy(); - const wchar_t* window_class = + const wchar_t *window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), @@ -117,32 +149,45 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), + WS_OVERLAPPEDWINDOW, // [window_manager] do not add WS_VISIBLE since the window will be shown later Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); - if (!window) { + if (!window) + { return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() +{ + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); + LPARAM const lparam) noexcept +{ + if (message == WM_NCCREATE) + { + auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); - auto that = static_cast(window_struct->lpCreateParams); + auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { + } + else if (Win32Window *that = GetThisFromHandle(window)) + { return that->MessageHandler(window, message, wparam, lparam); } @@ -153,64 +198,80 @@ LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; + LPARAM const lparam) noexcept +{ + switch (message) + { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) + { + PostQuitMessage(0); } + return 0; - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; + case WM_DPICHANGED: + { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: + { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) + { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) + { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } -void Win32Window::Destroy() { +void Win32Window::Destroy() +{ OnDestroy(); - if (window_handle_) { + if (window_handle_) + { DestroyWindow(window_handle_); window_handle_ = nullptr; } - if (g_active_window_count == 0) { + if (g_active_window_count == 0) + { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( +Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept +{ + return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } -void Win32Window::SetChildContent(HWND content) { +void Win32Window::SetChildContent(HWND content) +{ child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); @@ -221,25 +282,47 @@ void Win32Window::SetChildContent(HWND content) { SetFocus(child_content_); } -RECT Win32Window::GetClientArea() { +RECT Win32Window::GetClientArea() +{ RECT frame; GetClientRect(window_handle_, &frame); return frame; } -HWND Win32Window::GetHandle() { +HWND Win32Window::GetHandle() +{ return window_handle_; } -void Win32Window::SetQuitOnClose(bool quit_on_close) { +void Win32Window::SetQuitOnClose(bool quit_on_close) +{ quit_on_close_ = quit_on_close; } -bool Win32Window::OnCreate() { +bool Win32Window::OnCreate() +{ // No-op; provided for subclasses. return true; } -void Win32Window::OnDestroy() { +void Win32Window::OnDestroy() +{ // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) +{ + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) + { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/auth/windows/runner/win32_window.h b/auth/windows/runner/win32_window.h index 17ba43112..e3745cd06 100644 --- a/auth/windows/runner/win32_window.h +++ b/auth/windows/runner/win32_window.h @@ -10,15 +10,18 @@ // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling -class Win32Window { - public: - struct Point { +class Win32Window +{ +public: + struct Point + { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; - struct Size { + struct Size + { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) @@ -28,15 +31,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring &title, const Point &origin, const Size &size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -54,7 +58,7 @@ class Win32Window { // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); - protected: +protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. @@ -70,13 +74,13 @@ class Win32Window { // Called when Destroy is called. virtual void OnDestroy(); - private: +private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -84,7 +88,10 @@ class Win32Window { LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; + static Win32Window *GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); bool quit_on_close_ = false; @@ -95,4 +102,4 @@ class Win32Window { HWND child_content_ = nullptr; }; -#endif // RUNNER_WIN32_WINDOW_H_ +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/cli/internal/api/file_type.go b/cli/internal/api/file_type.go index f241f8fe5..93a23b7b0 100644 --- a/cli/internal/api/file_type.go +++ b/cli/internal/api/file_type.go @@ -18,6 +18,10 @@ type File struct { Info *FileInfo `json:"info,omitempty"` } +func (f File) IsRemovedFromAlbum() bool { + return f.IsDeleted || f.File.EncryptedData == "-" +} + // FileInfo has information about storage used by the file & it's metadata(future) type FileInfo struct { FileSize int64 `json:"fileSize,omitempty"` diff --git a/cli/internal/crypto/crypto.go b/cli/internal/crypto/crypto.go index 83200896b..11868c0ba 100644 --- a/cli/internal/crypto/crypto.go +++ b/cli/internal/crypto/crypto.go @@ -98,7 +98,8 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte, // Decode data from base64 dataBytes, err := base64.StdEncoding.DecodeString(data) if err != nil { - return "", nil, fmt.Errorf("invalid data: %v", err) + // safe to log the encrypted data + return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err) } // Decode nonce from base64 nonceBytes, err := base64.StdEncoding.DecodeString(nonce) diff --git a/cli/main.go b/cli/main.go index 157c11fd8..d62cdcffa 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,7 +15,7 @@ import ( "strings" ) -var AppVersion = "0.1.12" +var AppVersion = "0.1.13" func main() { cliDBPath, err := GetCLIConfigPath() diff --git a/cli/pkg/mapper/photo.go b/cli/pkg/mapper/photo.go index fa55ffae5..ac16f44f7 100644 --- a/cli/pkg/mapper/photo.go +++ b/cli/pkg/mapper/photo.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "github.com/ente-io/cli/internal/api" eCrypto "github.com/ente-io/cli/internal/crypto" "github.com/ente-io/cli/pkg/model" @@ -41,7 +42,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder if collection.MagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.MagicMetadata.Data, collectionKey, collection.MagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.PrivateMeta) if err != nil { @@ -51,28 +52,28 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder if collection.PublicMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.PublicMagicMetadata.Data, collectionKey, collection.PublicMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt public magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.PublicMeta) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal public magic metadata for collection %d: %w", collection.ID, err) } } if album.IsShared && collection.SharedMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.SharedMagicMetadata.Data, collectionKey, collection.SharedMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt shared magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.SharedMeta) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal shared magic metadata for collection %d: %w", collection.ID, err) } } return &album, nil } func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file api.File, holder *secrets.KeyHolder) (*model.RemoteFile, error) { - if file.IsDeleted { + if file.IsRemovedFromAlbum() { return nil, errors.New("file is deleted") } albumKey := album.AlbumKey.MustDecrypt(holder.DeviceKey) @@ -99,7 +100,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.Metadata.DecryptionHeader != "" { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.Metadata.EncryptedData, fileKey, file.Metadata.DecryptionHeader) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.Metadata) if err != nil { @@ -109,7 +110,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.MagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.MagicMetadata.Data, fileKey, file.MagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt magic metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.PrivateMetadata) if err != nil { @@ -119,7 +120,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.PubicMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.PubicMagicMetadata.Data, fileKey, file.PubicMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt public magic metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.PublicMetadata) if err != nil { diff --git a/cli/pkg/remote_sync.go b/cli/pkg/remote_sync.go index a37a4a58c..5ca149d71 100644 --- a/cli/pkg/remote_sync.go +++ b/cli/pkg/remote_sync.go @@ -87,16 +87,16 @@ func (c *ClICtrl) fetchRemoteFiles(ctx context.Context) error { if file.UpdationTime > maxUpdated { maxUpdated = file.UpdationTime } - if isFirstSync && file.IsDeleted { + if isFirstSync && file.IsRemovedFromAlbum() { // on first sync, no need to sync delete markers continue } - albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsDeleted, SyncedLocally: false} + albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsRemovedFromAlbum(), SyncedLocally: false} putErr := c.UpsertAlbumEntry(ctx, &albumEntry) if putErr != nil { return putErr } - if file.IsDeleted { + if file.IsRemovedFromAlbum() { continue } photoFile, err := mapper.MapApiFileToPhotoFile(ctx, album, file, c.KeyHolder) diff --git a/desktop/.eslintignore b/desktop/.eslintignore deleted file mode 100644 index d5314d13e..000000000 --- a/desktop/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -ui/* diff --git a/desktop/.eslintrc b/desktop/.eslintrc deleted file mode 100644 index e3916e17c..000000000 --- a/desktop/.eslintrc +++ /dev/null @@ -1,55 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "google", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "indent": "off", - "class-methods-use-this": "off", - "react/prop-types": "off", - "react/display-name": "off", - "react/no-unescaped-entities": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], - "require-jsdoc": "off", - "valid-jsdoc": "off", - "max-len": "off", - "new-cap": "off", - "no-invalid-this": "off", - "eqeqeq": "error", - "object-curly-spacing": ["error", "always"], - "space-before-function-paren": "off", - "operator-linebreak": [ - "error", - "after", - { "overrides": { "?": "before", ":": "before" } } - ] - }, - "settings": { - "react": { - "version": "detect" - } - }, - "globals": { - "JSX": "readonly", - "NodeJS": "readonly", - "ReadableStreamDefaultController": "readonly" - } -} diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js new file mode 100644 index 000000000..491d9aed3 --- /dev/null +++ b/desktop/.eslintrc.js @@ -0,0 +1,31 @@ +/* eslint-env node */ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + /* What we really want eventually */ + // "plugin:@typescript-eslint/strict-type-checked", + // "plugin:@typescript-eslint/stylistic-type-checked", + ], + /* Temporarily disable some rules + Enhancement: Remove me */ + rules: { + "no-unused-vars": "off", + }, + /* Temporarily add a global + Enhancement: Remove me */ + globals: { + NodeJS: "readonly", + }, + plugins: ["@typescript-eslint"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, + root: true, + ignorePatterns: [".eslintrc.js", "app", "out", "dist"], + env: { + es2022: true, + node: true, + }, +}; diff --git a/desktop/.gitignore b/desktop/.gitignore index 9b7e0cc60..f81ee56ca 100644 --- a/desktop/.gitignore +++ b/desktop/.gitignore @@ -14,7 +14,8 @@ node_modules/ # tsc transpiles src/**/*.ts and emits the generated JS into app app/ -# out is a symlink to the photos web app's dir +# out is a symlink to the photos web app's out dir, which contains the built up +# photos app. out # electron-builder diff --git a/desktop/.prettierrc.json b/desktop/.prettierrc.json index 8b0652597..7cf8c86c7 100644 --- a/desktop/.prettierrc.json +++ b/desktop/.prettierrc.json @@ -1,5 +1,6 @@ { "tabWidth": 4, + "proseWrap": "always", "plugins": [ "prettier-plugin-organize-imports", "prettier-plugin-packagejson" diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 73aae2397..83d2123d8 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -131,7 +131,8 @@ ### Photo Editor -Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) to know about feature and functionalities. +Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) +to know about feature and functionalities. ## v1.6.47 @@ -146,15 +147,19 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) ### Bug Fixes -- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379) +- Fixes OOM crashes during file upload + [#1379](https://github.com/ente-io/photos-web/pull/1379) ## v1.6.45 ### Bug Fixes -- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235) -- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378) -- Added view crash report option (help menu) for user to share electron crash report locally +- Fixed app keeps reloading issue + [#235](https://github.com/ente-io/photos-desktop/pull/235) +- Fixed dng and arw preview issue + [#1378](https://github.com/ente-io/photos-web/pull/1378) +- Added view crash report option (help menu) for user to share electron crash + report locally ## v1.6.44 @@ -166,23 +171,28 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) - #### Check for update and changelog option - Added options to check for update manually and a view changelog via the app menubar + Added options to check for update manually and a view changelog via the app + menubar - #### Opt out of crash reporting - Added option to out of a crash reporting, it can accessed from the settings -> preferences -> disable crash reporting + Added option to out of a crash reporting, it can accessed from the settings + -> preferences -> disable crash reporting - #### Type search - Added new search option to search files based on file type i.e, image, video, live-photo. + Added new search option to search files based on file type i.e, image, + video, live-photo. - #### Manual Convert Button - In case the video is not playable, Now there is a convert button which can be used to trigger conversion of the video to supported format. + In case the video is not playable, Now there is a convert button which can + be used to trigger conversion of the video to supported format. - #### File Download Progress - The file loader now also shows the exact percentage download progress, instead of just a simple loader. + The file loader now also shows the exact percentage download progress, + instead of just a simple loader. - #### Bug fixes & other enhancements @@ -198,16 +208,19 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) - #### Email verification - We have now made email verification optional, so you can sign in with just your email address and password, without waiting for a verification code. + We have now made email verification optional, so you can sign in with just + your email address and password, without waiting for a verification code. You can opt in / out of email verification from Settings > Security. - #### Download Album - You can now chose the download location for downloading albums. Along with that we have also added progress bar for album download. + You can now chose the download location for downloading albums. Along with + that we have also added progress bar for album download. - #### Bug fixes & other enhancements We have squashed a few pesky bugs that were reported by our community - If you would like to help us improve ente, come join the party @ ente.io/community! + If you would like to help us improve ente, come join the party @ + ente.io/community! diff --git a/desktop/README.md b/desktop/README.md index da74b133f..05149f5d0 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -2,31 +2,38 @@ The sweetness of Ente Photos, right on your computer. Linux, Windows and macOS. -You can [**download** a pre-built binary from -releases](https://github.com/ente-io/photos-desktop/releases/latest). +You can +[**download** a pre-built binary from releases](https://github.com/ente-io/photos-desktop/releases/latest). To know more about Ente, see [our main README](../README.md) or visit [ente.io](https://ente.io). ## Building from source +> [!CAUTION] +> +> We're improving the security of the desktop app further by migrating to +> Electron's sandboxing and contextIsolation. These updates are still WIP and +> meanwhile the instructions below might not fully work on the main branch. + +Fetch submodules + +```sh +git submodule update --init --recursive +``` + Install dependencies ```sh yarn install ``` -Run in development mode (with hot reload) +Run in development mode (supports hot reload for the renderer process) ```sh yarn dev ``` -> [!CAUTION] -> -> `yarn dev` is currently not working (we'll fix soon). If you just want to -> build from source and use the generated binary, use `yarn build`. - Or create a binary for your platform ```sh diff --git a/desktop/build/error.html b/desktop/build/error.html deleted file mode 100644 index cf1ea149d..000000000 --- a/desktop/build/error.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - ente Photos - - - -
-
-
- Site unreachable, please try again later -
- -
-
- - diff --git a/desktop/build/icon.icns b/desktop/build/icon.icns deleted file mode 100644 index ab7eface7..000000000 Binary files a/desktop/build/icon.icns and /dev/null differ diff --git a/desktop/build/splash.html b/desktop/build/splash.html deleted file mode 100644 index 199c31601..000000000 --- a/desktop/build/splash.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - ente Photos - - - -
-
- - - - - -
-
- - diff --git a/desktop/build/version.html b/desktop/build/version.html deleted file mode 100644 index b2038edd7..000000000 --- a/desktop/build/version.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Electron Updater Example - - - Current version: vX.Y.Z -
- - - diff --git a/desktop/build/window-icon.png b/desktop/build/window-icon.png deleted file mode 100644 index 5b0458033..000000000 Binary files a/desktop/build/window-icon.png and /dev/null differ diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index 6b0196723..cf38fe121 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -1,14 +1,123 @@ # Dependencies -See [web/docs/dependencies.md](../../web/docs/dependencies.md) for general web -specific dependencies. See [electron.md](electron.md) for our main dependency, -Electron. The rest of this document describes the remaining, desktop specific -dependencies that are used by the Photos desktop app. +## Electron -## Electron related +[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows, +macOS) way for creating desktop apps using TypeScript. + +Electron embeds Chromium and Node.js in the generated app's binary. The +generated app thus consists of two separate processes - the _main_ process, and +a _renderer_ process. + +- The _main_ process is runs the embedded node. This process can deal with the + host OS - it is conceptually like a `node` repl running on your machine. In + our case, the TypeScript code (in the `src/` directory) gets transpiled by + `tsc` into JavaScript in the `build/app/` directory, which gets bundled in + the generated app's binary and is loaded by the node (main) process when the + app starts. + +- The _renderer_ process is a regular web app that gets loaded into the + embedded Chromium. When the main process starts, it creates a new "window" + that shows this embedded Chromium. In our case, we build and bundle a static + export of the [Photos web app](../web/README.md) in the generated app. This + gets loaded by the embedded Chromium at runtime, acting as the app's UI. + +There is also a third environment that gets temporarily created: + +- The [preload script](../src/preload.ts) acts as a gateway between the _main_ + and the _renderer_ process. It runs in its own isolated environment. + +### Packaging + +[Electron Builder](https://www.electron.build) is used for packaging the app for +distribution. + +During the build it uses +[electron-builder-notarize](https://github.com/karaggeorge/electron-builder-notarize) +to notarize the macOS binary. + +### Updates + +[electron-updater](https://www.electron.build/auto-update#debugging), while a +separate package, is also a part of Electron Builder. It provides an alternative +to Electron's built in auto updater, with a more flexible API. It supports auto +updates for the DMG, AppImage, DEB, RPM and NSIS packages. + +[compare-versions](https://github.com/omichelsen/compare-versions) is used for +semver comparisons when we decide when to trigger updates. + +### Logging + +[electron-log](https://github.com/megahertz/electron-log) is used for logging. +Specifically, it allows us to log to a file (in addition to the console of the +Node.js process), and also handles log rotation and limiting the size of the log +files. ### next-electron-server This spins up a server for serving files using a protocol handler inside our -Electron process. This allows us to directly use the output produced by `next -build` for loading into our renderer process. +Electron process. This allows us to directly use the output produced by +`next build` for loading into our renderer process. + +### Others + +* [any-shell-escape](https://github.com/boazy/any-shell-escape) is for + escaping shell commands before we execute them (e.g. say when invoking the + embedded ffmpeg CLI). + +* [auto-launch](https://github.com/Teamwork/node-auto-launch) is for + automatically starting our app on login, if the user so wishes. + +* [electron-store](https://github.com/sindresorhus/electron-store) is used for + persisting user preferences and other arbitrary data. + +## Dev + +See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dev) for the +general development experience related dependencies like TypeScript etc, which +are similar to that in the web code. + +Some extra ones specific to the code here are: + +* [concurrently](https://github.com/open-cli-tools/concurrently) for spawning + parallel tasks when we do `yarn dev`. + +* [shx](https://github.com/shelljs/shx) for providing a portable way to use Unix + commands in our `package.json` scripts. This allows us to use the same + commands (like `ln`) across different platforms like Linux and Windows. + +## Functionality + +### Conversion + +The main tool we use is for arbitrary conversions is FFMPEG. To bundle a +(platform specific) static binary of ffmpeg with our app, we use +[ffmpeg-static](https://github.com/eugeneware/ffmpeg-static). + +> There is a significant (~20x) speed difference between using the compiled +> FFMPEG binary and using the WASM one (that our renderer process already has). +> Which is why we bundle it to speed up operations on the desktop app. + +In addition, we also bundle a static Linux binary of imagemagick in our extra +resources (`build`) folder. This is used for thumbnail generation on Linux. + +On macOS, we use the `sips` CLI tool for conversion, but that is already +available on the host machine, and is not bundled with our app. + +### Watch Folders + +[chokidar](https://github.com/paulmillr/chokidar) is used as a file system +watcher for the watch folders functionality. + +### AI/ML + +* [onnxruntime-node](https://github.com/Microsoft/onnxruntime) +* html-entities is used by the bundled clip-bpe-ts. +* GGML binaries are bundled +* We also use [jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) for + conversion of all images to JPEG before processing. + +## ZIP + +[node-stream-zip](https://github.com/antelle/node-stream-zip) is used for +reading of large ZIP files (e.g. during imports of Google Takeout ZIPs). diff --git a/desktop/docs/dev.md b/desktop/docs/dev.md index bfa80df69..507438fdc 100644 --- a/desktop/docs/dev.md +++ b/desktop/docs/dev.md @@ -1,4 +1,38 @@ -# Development tips +# Development -- `yarn build:quick` is a variant of `yarn build` that uses the - `--config.compression=store` flag to (slightly) speed up electron-builder. +## Yarn commands + +### yarn dev + +Launch the app in development mode: + +- Transpiles the files in `src/` and starts the main process. + +- Runs a development server for the renderer, with hot module reload. + +### yarn build + +Build a binary for your current platform. + +Note that our actual releases use a +[GitHub workflow](../.github/workflows/desktop-release.yml) that is similar to +this, except it builds binaries for all the supported OSes and uses production +signing credentials. + +During development, you might find `yarn build:quick` helpful. It is a variant +of `yarn build` that omits some steps to build a binary quicker, something that +can be useful during development. + +### postinstall + +When using native node modules (those written in C/C++), we need to ensure they +are built against `electron`'s packaged `node` version. We use +[electron-builder](https://www.electron.build/cli)'s `install-app-deps` command +to rebuild those modules automatically after each `yarn install` by invoking it +in as the `postinstall` step in our package.json. + +### lint, lint-fix + +Use `yarn lint` to check that your code formatting is as expected, and that +there are no linter errors. Use `yarn lint-fix` to try and automatically fix the +issues. diff --git a/desktop/docs/electron.md b/desktop/docs/electron.md deleted file mode 100644 index 84c47e329..000000000 --- a/desktop/docs/electron.md +++ /dev/null @@ -1,21 +0,0 @@ -# Electron - -[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows, -macOS) way for creating desktop apps using TypeScript. - -Electron embeds Chromium and Node.js in the generated app's binary. The -generated app thus consists of two separate processes - the _main_ process, and -a _renderer_ process. - -- The _main_ process is runs the embedded node. This process can deal with the - host OS - it is conceptually like a `node` repl running on your machine. In our - case, the TypeScript code (in the `src/` directory) gets transpiled by `tsc` - into JavaScript in the `build/app/` directory, which gets bundled in the - generated app's binary and is loaded by the node (main) process when the app - starts. - -- The _renderer_ process is a regular web app that gets loaded into the embedded - Chromium. When the main process starts, it creates a new "window" that shows - this embedded Chromium. In our case, we build and bundle a static export of - the [Photos web app](../web/README.md) in the generated app. This gets loaded - by the embedded Chromium at runtime, acting as the app's UI. diff --git a/desktop/docs/release.md b/desktop/docs/release.md new file mode 100644 index 000000000..7254e26fc --- /dev/null +++ b/desktop/docs/release.md @@ -0,0 +1,92 @@ +## Releases + +> [!NOTE] +> +> TODO(MR): This document needs to be audited and changed as we do the first +> release from this new monorepo. + +The Github Action that builds the desktop binaries is triggered by pushing a tag +matching the pattern `photos-desktop-v1.2.3`. This value should match the +version in `package.json`. + +So the process for doing a release would be. + +1. Create a new branch (can be named anything). On this branch, include your + changes. + +2. Mention the changes in `CHANGELOG.md`. + +3. Changing the `version` in `package.json` to `1.x.x`. + +4. Commit and push to remote + + ```sh + git add package.json && git commit -m 'Release v1.x.x' + git tag v1.x.x + git push && git push --tags + ``` + +This by itself will already trigger a new release. The GitHub action will create +a new draft release that can then be used as descibed below. + +To wrap up, we also need to merge back these changes into main. So for that, + +5. Open a PR for the branch that we're working on (where the above tag was + pushed from) to get it merged into main. + +6. In this PR, also increase the version number for the next release train. That + is, supposed we just released `v4.0.1`. Then we'll change the version number + in main to `v4.0.2-next.0`. Each pre-release will modify the `next.0` part. + Finally, at the time of the next release, this'll become `v4.0.2`. + +The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts +defined in the `build` value in `package.json`. + +- Windows - An NSIS installer. +- Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`) +- macOS - A universal DMG + +Additionally, the GitHub action notarizes the macOS DMG. For this it needs +credentials provided via GitHub secrets. + +During the build the Sentry webpack plugin checks to see if SENTRY_AUTH_TOKEN is +defined. If so, it uploads the sourcemaps for the renderer process to Sentry +(For our GitHub action, the SENTRY_AUTH_TOKEN is defined as a GitHub secret). + +The sourcemaps for the main (node) process are currently not sent to Sentry +(this works fine in practice since the node process files are not minified, we +only run `tsc`). + +Once the build is done, a draft release with all these artifacts attached is +created. The build is idempotent, so if something goes wrong and we need to +re-run the GitHub action, just delete the draft release (if it got created) and +start a new run by pushing a new tag (if some code changes are required). + +If no code changes are required, say the build failed for some transient network +or sentry issue, we can even be re-run by the build by going to Github Action +age and rerun from there. This will re-trigger for the same tag. + +If everything goes well, we'll have a release on GitHub, and the corresponding +source maps for the renderer process uploaded to Sentry. There isn't anything +else to do: + +- The website automatically redirects to the latest release on GitHub when + people try to download. + +- The file formats with support auto update (Windows `exe`, the Linux AppImage + and the macOS DMG) also check the latest GitHub release automatically to + download and apply the update (the rest of the formats don't support auto + updates). + +- We're not putting the desktop app in other stores currently. It is available + as a `brew cask`, but we only had to open a PR to add the initial formula, + now their maintainers automatically bump the SHA, version number and the + (derived from the version) URL in the formula when their tools notice a new + release on our GitHub. + +We can also publish the draft releases by checking the "pre-release" option. +Such releases don't cause any of the channels (our website, or the desktop app +auto updater, or brew) to be notified, instead these are useful for giving links +to pre-release builds to customers. Generally, in the version number for these +we'll add a label to the version, e.g. the "beta.x" in `1.x.x-beta.x`. This +should be done both in `package.json`, and what we tag the commit with. diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml new file mode 100644 index 000000000..9189c3435 --- /dev/null +++ b/desktop/electron-builder.yml @@ -0,0 +1,29 @@ +appId: io.ente.bhari-frame +artifactName: ${productName}-${version}-${arch}.${ext} +nsis: + deleteAppDataOnUninstall: true +linux: + target: + - target: AppImage + arch: [x64, arm64] + - target: deb + arch: [x64, arm64] + - target: rpm + arch: [x64, arm64] + - target: pacman + arch: [x64, arm64] + category: Photography +mac: + target: + target: default + arch: [universal] + category: public.app-category.photography + hardenedRuntime: true + x64ArchFiles: Contents/Resources/ggmlclip-mac +afterSign: electron-builder-notarize +extraFiles: + - from: build + to: resources +files: + - app/**/* + - out diff --git a/desktop/package.json b/desktop/package.json index 8d90fca6e..16ba23eb9 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -8,128 +8,46 @@ "scripts": { "build": "yarn build-renderer && yarn build-main", "build-main": "tsc && electron-builder", - "build-main:quick": "tsc && electron-builder --config.compression=store", - "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && rm -f out && ln -sf ../web/apps/photos/out", + "build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null", + "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -f out && shx ln -sf ../web/apps/photos/out out", "build:quick": "yarn build-renderer && yarn build-main:quick", - "dev": "concurrently --names 'main,rndr,tscw' \"yarn dev-main\" \"yarn dev-renderer\" \"yarn dev-main-watch\"", + "dev": "concurrently --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"", "dev-main": "tsc && electron app/main.js", - "dev-main-watch": "tsc --watch --preserveWatchOutput", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", - "lint": "yarn prettier --check . && eslint \"src/**/*.ts\"", - "lint-fix": "yarn prettier --write . && eslint --fix src" + "lint": "yarn prettier --check . && eslint --ext .ts src", + "lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src" }, "dependencies": { - "any-shell-escape": "^0.1.1", - "auto-launch": "^5.0.5", - "chokidar": "^3.5.3", - "compare-versions": "^6.1.0", - "electron-log": "^4.3.5", - "electron-reload": "^2.0.0-alpha.1", - "electron-store": "^8.0.1", - "electron-updater": "^4.3.8", - "ffmpeg-static": "^5.1.0", - "get-folder-size": "^2.0.1", - "html-entities": "^2.4.0", - "jpeg-js": "^0.4.4", + "any-shell-escape": "^0.1", + "auto-launch": "^5.0", + "chokidar": "^3.6", + "compare-versions": "^6.1", + "electron-log": "^5.1", + "electron-store": "^8.2", + "electron-updater": "^6.1", + "ffmpeg-static": "^5.2", + "html-entities": "^2.5", + "jpeg-js": "^0.4", "next-electron-server": "^1", - "node-fetch": "^2.6.7", - "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.16.3", - "promise-fs": "^2.1.1" + "node-stream-zip": "^1.15", + "onnxruntime-node": "^1.17" }, "devDependencies": { - "@types/auto-launch": "^5.0.2", - "@types/ffmpeg-static": "^3.0.1", - "@types/get-folder-size": "^2.0.0", - "@types/node": "18.15.0", - "@types/node-fetch": "^2.6.2", - "@types/promise-fs": "^2.1.1", - "@typescript-eslint/eslint-plugin": "^5.28.0", - "@typescript-eslint/parser": "^5.28.0", - "concurrently": "^7.0.0", - "electron": "^25.8.4", - "electron-builder": "^24.6.4", - "electron-builder-notarize": "^1.2.0", - "electron-download": "^4.1.1", - "eslint": "^7.23.0", - "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^8.5.0", + "@types/auto-launch": "^5.0", + "@types/ffmpeg-static": "^3.0", + "@typescript-eslint/eslint-plugin": "^7", + "@typescript-eslint/parser": "^7", + "concurrently": "^8", + "electron": "^29", + "electron-builder": "^24", + "electron-builder-notarize": "^1.5", + "eslint": "^8", "prettier": "^3", "prettier-plugin-organize-imports": "^3.2", "prettier-plugin-packagejson": "^2.4", - "typescript": "^4.2.3" + "shx": "^0.3", + "typescript": "^5" }, - "build": { - "appId": "io.ente.bhari-frame", - "artifactName": "${productName}-${version}-${arch}.${ext}", - "nsis": { - "deleteAppDataOnUninstall": true - }, - "linux": { - "target": [ - { - "target": "AppImage", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "deb", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "rpm", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "pacman", - "arch": [ - "x64", - "arm64" - ] - } - ], - "icon": "./resources/icon.icns", - "category": "Photography" - }, - "mac": { - "target": { - "target": "default", - "arch": [ - "universal" - ] - }, - "category": "public.app-category.photography", - "hardenedRuntime": true, - "x64ArchFiles": "Contents/Resources/ggmlclip-mac" - }, - "afterSign": "electron-builder-notarize", - "asarUnpack": [ - "node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg", - "node_modules/ffmpeg-static/index.js", - "node_modules/ffmpeg-static/package.json" - ], - "extraFiles": [ - { - "from": "build", - "to": "resources" - } - ], - "files": [ - "app/**/*", - "out" - ] - }, - "productName": "ente", - "standard": { - "parser": "babel-eslint" - } + "productName": "ente" } diff --git a/desktop/src/api/cache.ts b/desktop/src/api/cache.ts deleted file mode 100644 index 86ba4378c..000000000 --- a/desktop/src/api/cache.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import path from "path"; -import { existsSync, mkdir, rmSync } from "promise-fs"; -import { DiskCache } from "../services/diskCache"; - -const ENTE_CACHE_DIR_NAME = "ente"; - -export const getCacheDirectory = async () => { - const customCacheDir = await getCustomCacheDirectory(); - if (customCacheDir && existsSync(customCacheDir)) { - return customCacheDir; - } - const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache"); - return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME); -}; - -const getCacheBucketDir = async (cacheName: string) => { - const cacheDir = await getCacheDirectory(); - const cacheBucketDir = path.join(cacheDir, cacheName); - return cacheBucketDir; -}; - -export async function openDiskCache( - cacheName: string, - cacheLimitInBytes?: number, -) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - if (!existsSync(cacheBucketDir)) { - await mkdir(cacheBucketDir, { recursive: true }); - } - return new DiskCache(cacheBucketDir, cacheLimitInBytes); -} - -export async function deleteDiskCache(cacheName: string) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - if (existsSync(cacheBucketDir)) { - rmSync(cacheBucketDir, { recursive: true, force: true }); - return true; - } else { - return false; - } -} - -export async function setCustomCacheDirectory( - directory: string, -): Promise { - await ipcRenderer.invoke("set-custom-cache-directory", directory); -} - -async function getCustomCacheDirectory(): Promise { - return await ipcRenderer.invoke("get-custom-cache-directory"); -} diff --git a/desktop/src/api/clip.ts b/desktop/src/api/clip.ts deleted file mode 100644 index d2469e7b9..000000000 --- a/desktop/src/api/clip.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ipcRenderer } from "electron"; -import { writeStream } from "../services/fs"; -import { Model } from "../types"; -import { isExecError, parseExecError } from "../utils/error"; - -export async function computeImageEmbedding( - model: Model, - imageData: Uint8Array, -): Promise { - let tempInputFilePath = null; - try { - tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", ""); - const imageStream = new Response(imageData.buffer).body; - await writeStream(tempInputFilePath, imageStream); - const embedding = await ipcRenderer.invoke( - "compute-image-embedding", - model, - tempInputFilePath, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } finally { - if (tempInputFilePath) { - await ipcRenderer.invoke("remove-temp-file", tempInputFilePath); - } - } -} - -export async function computeTextEmbedding( - model: Model, - text: string, -): Promise { - try { - const embedding = await ipcRenderer.invoke( - "compute-text-embedding", - model, - text, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } -} diff --git a/desktop/src/api/common.ts b/desktop/src/api/common.ts deleted file mode 100644 index f18506981..000000000 --- a/desktop/src/api/common.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { logError } from "../services/logging"; - -export const selectDirectory = async (): Promise => { - try { - return await ipcRenderer.invoke("select-dir"); - } catch (e) { - logError(e, "error while selecting root directory"); - } -}; - -export const getAppVersion = async (): Promise => { - try { - return await ipcRenderer.invoke("get-app-version"); - } catch (e) { - logError(e, "failed to get release version"); - throw e; - } -}; - -export const openDirectory = async (dirPath: string): Promise => { - try { - await ipcRenderer.invoke("open-dir", dirPath); - } catch (e) { - logError(e, "error while opening directory"); - throw e; - } -}; - -export const getPlatform = async (): Promise<"mac" | "windows" | "linux"> => { - try { - return await ipcRenderer.invoke("get-platform"); - } catch (e) { - logError(e, "failed to get platform"); - throw e; - } -}; - -export { logToDisk, openLogDirectory } from "../services/logging"; diff --git a/desktop/src/api/electronStore.ts b/desktop/src/api/electronStore.ts index 5f84776e1..2ee74953d 100644 --- a/desktop/src/api/electronStore.ts +++ b/desktop/src/api/electronStore.ts @@ -1,4 +1,4 @@ -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { keysStore } from "../stores/keys.store"; import { safeStorageStore } from "../stores/safeStorage.store"; import { uploadStatusStore } from "../stores/upload.store"; diff --git a/desktop/src/api/export.ts b/desktop/src/api/export.ts deleted file mode 100644 index 8adaa236f..000000000 --- a/desktop/src/api/export.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as fs from "promise-fs"; -import { writeStream } from "./../services/fs"; - -export const exists = (path: string) => { - return fs.existsSync(path); -}; - -export const checkExistsAndCreateDir = async (dirPath: string) => { - if (!fs.existsSync(dirPath)) { - await fs.mkdir(dirPath); - } -}; - -export const saveStreamToDisk = async ( - filePath: string, - fileStream: ReadableStream, -) => { - await writeStream(filePath, fileStream); -}; - -export const saveFileToDisk = async (path: string, fileData: string) => { - await fs.writeFile(path, fileData); -}; diff --git a/desktop/src/api/ffmpeg.ts b/desktop/src/api/ffmpeg.ts deleted file mode 100644 index 9d11183a8..000000000 --- a/desktop/src/api/ffmpeg.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ipcRenderer } from "electron"; -import { existsSync } from "fs"; -import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; -import { ElectronFile } from "../types"; - -export async function runFFmpegCmd( - cmd: string[], - inputFile: File | ElectronFile, - outputFileName: string, - dontTimeout?: boolean, -) { - let inputFilePath = null; - let createdTempInputFile = null; - try { - if (!existsSync(inputFile.path)) { - const tempFilePath = await ipcRenderer.invoke( - "get-temp-file-path", - inputFile.name, - ); - await writeStream(tempFilePath, await inputFile.stream()); - inputFilePath = tempFilePath; - createdTempInputFile = true; - } else { - inputFilePath = inputFile.path; - } - const outputFileData = await ipcRenderer.invoke( - "run-ffmpeg-cmd", - cmd, - inputFilePath, - outputFileName, - dontTimeout, - ); - return new File([outputFileData], outputFileName); - } finally { - if (createdTempInputFile) { - try { - await ipcRenderer.invoke("remove-temp-file", inputFilePath); - } catch (e) { - logError(e, "failed to deleteTempFile"); - } - } - } -} diff --git a/desktop/src/api/fs.ts b/desktop/src/api/fs.ts deleted file mode 100644 index d9ec2eeec..000000000 --- a/desktop/src/api/fs.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getDirFilePaths, getElectronFile } from "../services/fs"; - -export async function getDirFiles(dirPath: string) { - const files = await getDirFilePaths(dirPath); - const electronFiles = await Promise.all(files.map(getElectronFile)); - return electronFiles; -} -export { - deleteFile, - deleteFolder, - isFolder, - moveFile, - readTextFile, - rename, -} from "../services/fs"; diff --git a/desktop/src/api/imageProcessor.ts b/desktop/src/api/imageProcessor.ts deleted file mode 100644 index 9d93aecd1..000000000 --- a/desktop/src/api/imageProcessor.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { existsSync } from "fs"; -import { CustomErrors } from "../constants/errors"; -import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; -import { ElectronFile } from "../types"; -import { isPlatform } from "../utils/common/platform"; - -export async function convertToJPEG( - fileData: Uint8Array, - filename: string, -): Promise { - if (isPlatform("windows")) { - throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED); - } - const convertedFileData = await ipcRenderer.invoke( - "convert-to-jpeg", - fileData, - filename, - ); - return convertedFileData; -} - -export async function generateImageThumbnail( - inputFile: File | ElectronFile, - maxDimension: number, - maxSize: number, -): Promise { - let inputFilePath = null; - let createdTempInputFile = null; - try { - if (isPlatform("windows")) { - throw Error( - CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED, - ); - } - if (!existsSync(inputFile.path)) { - const tempFilePath = await ipcRenderer.invoke( - "get-temp-file-path", - inputFile.name, - ); - await writeStream(tempFilePath, await inputFile.stream()); - inputFilePath = tempFilePath; - createdTempInputFile = true; - } else { - inputFilePath = inputFile.path; - } - const thumbnail = await ipcRenderer.invoke( - "generate-image-thumbnail", - inputFilePath, - maxDimension, - maxSize, - ); - return thumbnail; - } finally { - if (createdTempInputFile) { - try { - await ipcRenderer.invoke("remove-temp-file", inputFilePath); - } catch (e) { - logError(e, "failed to deleteTempFile"); - } - } - } -} diff --git a/desktop/src/api/safeStorage.ts b/desktop/src/api/safeStorage.ts index 64c489195..e3d674951 100644 --- a/desktop/src/api/safeStorage.ts +++ b/desktop/src/api/safeStorage.ts @@ -1,13 +1,11 @@ -import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { safeStorage } from "electron/main"; +import { logError } from "../main/log"; import { safeStorageStore } from "../stores/safeStorage.store"; export async function setEncryptionKey(encryptionKey: string) { try { - const encryptedKey: Buffer = await ipcRenderer.invoke( - "safeStorage-encrypt", - encryptionKey, - ); + const encryptedKey: Buffer = + await safeStorage.encryptString(encryptionKey); const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); safeStorageStore.set("encryptionKey", b64EncryptedKey); } catch (e) { @@ -20,10 +18,8 @@ export async function getEncryptionKey(): Promise { try { const b64EncryptedKey = safeStorageStore.get("encryptionKey"); if (b64EncryptedKey) { - const keyBuffer = new Uint8Array( - Buffer.from(b64EncryptedKey, "base64"), - ); - return await ipcRenderer.invoke("safeStorage-decrypt", keyBuffer); + const keyBuffer = Buffer.from(b64EncryptedKey, "base64"); + return await safeStorage.decryptString(keyBuffer); } } catch (e) { logError(e, "getEncryptionKey failed"); diff --git a/desktop/src/api/system.ts b/desktop/src/api/system.ts deleted file mode 100644 index a4dc91e05..000000000 --- a/desktop/src/api/system.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ipcRenderer } from "electron"; -import { AppUpdateInfo } from "../types"; - -export const sendNotification = (content: string) => { - ipcRenderer.send("send-notification", content); -}; -export const reloadWindow = () => { - ipcRenderer.send("reload-window"); -}; - -export const registerUpdateEventListener = ( - showUpdateDialog: (updateInfo: AppUpdateInfo) => void, -) => { - ipcRenderer.removeAllListeners("show-update-dialog"); - ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => { - showUpdateDialog(updateInfo); - }); -}; - -export const registerForegroundEventListener = (onForeground: () => void) => { - ipcRenderer.removeAllListeners("app-in-foreground"); - ipcRenderer.on("app-in-foreground", () => { - onForeground(); - }); -}; - -export const updateAndRestart = () => { - ipcRenderer.send("update-and-restart"); -}; - -export const skipAppUpdate = (version: string) => { - ipcRenderer.send("skip-app-update", version); -}; - -export const muteUpdateNotification = (version: string) => { - ipcRenderer.send("mute-update-notification", version); -}; diff --git a/desktop/src/api/upload.ts b/desktop/src/api/upload.ts index 280ff084f..24d0283ff 100644 --- a/desktop/src/api/upload.ts +++ b/desktop/src/api/upload.ts @@ -1,12 +1,10 @@ -import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { getElectronFile } from "../services/fs"; import { getElectronFilesFromGoogleZip, getSavedFilePaths, } from "../services/upload"; import { uploadStatusStore } from "../stores/upload.store"; -import { ElectronFile, FILE_PATH_TYPE } from "../types"; -import { getElectronFile } from "./../services/fs"; +import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc"; export const getPendingUploads = async () => { const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES); @@ -36,53 +34,6 @@ export const getPendingUploads = async () => { }; }; -export const showUploadDirsDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-dirs-dialog", - ); - const files = await Promise.all(filePaths.map(getElectronFile)); - return files; - } catch (e) { - logError(e, "error while selecting folders"); - } -}; - -export const showUploadFilesDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-files-dialog", - ); - const files = await Promise.all(filePaths.map(getElectronFile)); - return files; - } catch (e) { - logError(e, "error while selecting files"); - } -}; - -export const showUploadZipDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-zip-dialog", - ); - let files: ElectronFile[] = []; - - for (const filePath of filePaths) { - files = [ - ...files, - ...(await getElectronFilesFromGoogleZip(filePath)), - ]; - } - - return { - zipPaths: filePaths, - files, - }; - } catch (e) { - logError(e, "error while selecting zips"); - } -}; - export { getElectronFilesFromGoogleZip, setToUploadCollection, diff --git a/desktop/src/api/watch.ts b/desktop/src/api/watch.ts deleted file mode 100644 index 1b7a4ac3c..000000000 --- a/desktop/src/api/watch.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ipcRenderer } from "electron"; -import ElectronLog from "electron-log"; -import path from "path"; -import { getElectronFile } from "../services/fs"; -import { getWatchMappings, setWatchMappings } from "../services/watch"; -import { ElectronFile, WatchMapping } from "../types"; -import { isMappingPresent } from "../utils/watch"; - -export async function addWatchMapping( - rootFolderName: string, - folderPath: string, - uploadStrategy: number, -) { - ElectronLog.log(`Adding watch mapping: ${folderPath}`); - const watchMappings = getWatchMappings(); - if (isMappingPresent(watchMappings, folderPath)) { - throw new Error(`Watch mapping already exists`); - } - - await ipcRenderer.invoke("add-watcher", { - dir: folderPath, - }); - - watchMappings.push({ - rootFolderName, - uploadStrategy, - folderPath, - syncedFiles: [], - ignoredFiles: [], - }); - - setWatchMappings(watchMappings); -} - -export async function removeWatchMapping(folderPath: string) { - let watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw new Error(`Watch mapping does not exist`); - } - - await ipcRenderer.invoke("remove-watcher", { - dir: watchMapping.folderPath, - }); - - watchMappings = watchMappings.filter( - (mapping) => mapping.folderPath !== watchMapping.folderPath, - ); - - setWatchMappings(watchMappings); -} - -export function updateWatchMappingSyncedFiles( - folderPath: string, - files: WatchMapping["syncedFiles"], -): void { - const watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw Error(`Watch mapping not found`); - } - - watchMapping.syncedFiles = files; - setWatchMappings(watchMappings); -} - -export function updateWatchMappingIgnoredFiles( - folderPath: string, - files: WatchMapping["ignoredFiles"], -): void { - const watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw Error(`Watch mapping not found`); - } - - watchMapping.ignoredFiles = files; - setWatchMappings(watchMappings); -} - -export function registerWatcherFunctions( - addFile: (file: ElectronFile) => Promise, - removeFile: (path: string) => Promise, - removeFolder: (folderPath: string) => Promise, -) { - ipcRenderer.removeAllListeners("watch-add"); - ipcRenderer.removeAllListeners("watch-change"); - ipcRenderer.removeAllListeners("watch-unlink-dir"); - ipcRenderer.on("watch-add", async (_, filePath: string) => { - filePath = filePath.split(path.sep).join(path.posix.sep); - - await addFile(await getElectronFile(filePath)); - }); - ipcRenderer.on("watch-unlink", async (_, filePath: string) => { - filePath = filePath.split(path.sep).join(path.posix.sep); - - await removeFile(filePath); - }); - ipcRenderer.on("watch-unlink-dir", async (_, folderPath: string) => { - folderPath = folderPath.split(path.sep).join(path.posix.sep); - await removeFolder(folderPath); - }); -} - -export { getWatchMappings } from "../services/watch"; diff --git a/desktop/src/constants/errors.ts b/desktop/src/constants/errors.ts index 97aef616c..e6225ecf6 100644 --- a/desktop/src/constants/errors.ts +++ b/desktop/src/constants/errors.ts @@ -1,3 +1,17 @@ +/** + * [Note: Custom errors across Electron/Renderer boundary] + * + * We need to use the `message` field to disambiguate between errors thrown by + * the main process when invoked from the renderer process. This is because: + * + * > Errors thrown throw `handle` in the main process are not transparent as + * > they are serialized and only the `message` property from the original error + * > is provided to the renderer process. + * > + * > - https://www.electronjs.org/docs/latest/tutorial/ipc + * > + * > Ref: https://github.com/electron/electron/issues/24427 + */ export const CustomErrors = { WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED: "Windows native image processing is not supported", diff --git a/desktop/src/main.ts b/desktop/src/main.ts index a280a9b59..4383fa73f 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,27 +1,34 @@ -import { app, BrowserWindow } from "electron"; -import electronReload from "electron-reload"; +/** + * @file Entry point for the main (Node.js) process of our Electron app. + * + * The code in this file is invoked by Electron when our app starts - + * Conceptually (after all the transpilation etc has happened) this can be + * thought of `electron main.ts`. We're running in the context of the so called + * "main" process which runs in a Node.js environment. + * + * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process + */ +import { app, BrowserWindow, Menu } from "electron/main"; import serveNextAt from "next-electron-server"; -import { initWatcher } from "./services/chokidar"; -import { isDev } from "./utils/common"; -import { addAllowOriginHeader } from "./utils/cors"; -import { createWindow } from "./utils/createWindow"; -import { setupAppEventEmitter } from "./utils/events"; -import setupIpcComs from "./utils/ipcComms"; -import { setupLogging } from "./utils/logging"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; import { - enableSharedArrayBufferSupport, + addAllowOriginHeader, + createWindow, handleDockIconHideOnAutoLaunch, handleDownloads, handleExternalLinks, - handleUpdates, - logSystemInfo, + logStartupBanner, setupMacWindowOnDockIconClick, - setupMainMenu, setupTrayItem, -} from "./utils/main"; -import { setupMainProcessStatsLogger } from "./utils/processStats"; - -let mainWindow: BrowserWindow; +} from "./main/init"; +import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; +import log, { initLogging } from "./main/log"; +import { createApplicationMenu } from "./main/menu"; +import { isDev } from "./main/util"; +import { setupAutoUpdater } from "./services/appUpdater"; +import { initWatcher } from "./services/chokidar"; let appIsQuitting = false; @@ -43,19 +50,6 @@ export const setIsUpdateAvailable = (value: boolean): void => { updateIsAvailable = value; }; -/** - * Hot reload the main process if anything changes in the source directory that - * we're running from. - * - * In particular, this gets triggered when the `tsc -w` rebuilds JS files in the - * `app/` directory when we change the TS files in the `src/` directory. - */ -const setupMainHotReload = () => { - if (isDev) { - electronReload(__dirname, {}); - } -}; - /** * The URL where the renderer HTML is being served from. */ @@ -78,16 +72,82 @@ const setupRendererServer = () => { serveNextAt(rendererURL); }; -setupMainHotReload(); -setupRendererServer(); -setupLogging(isDev); +function enableSharedArrayBufferSupport() { + app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); +} -const gotTheLock = app.requestSingleInstanceLock(); -if (!gotTheLock) { - app.quit(); -} else { +/** + * [Note: Increased disk cache for the desktop app] + * + * Set the "disk-cache-size" command line flag to ask the Chromium process to + * use a larger size for the caches that it keeps on disk. This allows us to use + * the same web-native caching mechanism on both the web and the desktop app, + * just ask the embedded Chromium to be a bit more generous in disk usage when + * running as the desktop app. + * + * The size we provide is in bytes. We set it to a large value, 5 GB (5 * 1024 * + * 1024 * 1024 = 5368709120) + * https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize + * + * Note that increasing the disk cache size does not guarantee that Chromium + * will respect in verbatim, it uses its own heuristics atop this hint. + * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 + */ +const increaseDiskCache = () => { + app.commandLine.appendSwitch("disk-cache-size", "5368709120"); +}; + +/** + * Older versions of our app used to maintain a cache dir using the main + * process. This has been deprecated in favor of using a normal web cache. + * + * See [Note: Increased disk cache for the desktop app] + * + * Delete the old cache dir if it exists. This code was added March 2024, and + * can be removed after some time once most people have upgraded to newer + * versions. + */ +const deleteLegacyDiskCacheDirIfExists = async () => { + // The existing code was passing "cache" as a parameter to getPath. This is + // incorrect if we go by the types - "cache" is not a valid value for the + // parameter to `app.getPath`. + // + // It might be an issue in the types, since at runtime it seems to work. For + // example, on macOS I get `~/Library/Caches`. + // + // Irrespective, we replicate the original behaviour so that we get back the + // same path that the old got was getting. + // + // @ts-expect-error + const cacheDir = path.join(app.getPath("cache"), "ente"); + if (existsSync(cacheDir)) { + log.info(`Removing legacy disk cache from ${cacheDir}`); + await fs.rm(cacheDir, { recursive: true }); + } +}; + +function setupAppEventEmitter(mainWindow: BrowserWindow) { + // fire event when mainWindow is in foreground + mainWindow.on("focus", () => { + mainWindow.webContents.send("app-in-foreground"); + }); +} + +const main = () => { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + return; + } + + let mainWindow: BrowserWindow; + + initLogging(); + setupRendererServer(); handleDockIconHideOnAutoLaunch(); + increaseDiskCache(); enableSharedArrayBufferSupport(); + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { @@ -99,24 +159,34 @@ if (!gotTheLock) { } }); - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. + // Emitted once, when Electron has finished initializing. + // + // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { - logSystemInfo(); - setupMainProcessStatsLogger(); + logStartupBanner(); mainWindow = await createWindow(); - const tray = setupTrayItem(mainWindow); const watcher = initWatcher(mainWindow); + setupTrayItem(mainWindow); setupMacWindowOnDockIconClick(); - setupMainMenu(mainWindow); - setupIpcComs(tray, mainWindow, watcher); - await handleUpdates(mainWindow); + Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); + attachIPCHandlers(); + attachFSWatchIPCHandlers(watcher); + if (!isDev) setupAutoUpdater(mainWindow); handleDownloads(mainWindow); handleExternalLinks(mainWindow); addAllowOriginHeader(mainWindow); setupAppEventEmitter(mainWindow); + + try { + deleteLegacyDiskCacheDirIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions + log.error("Ignoring startup error", e); + } }); app.on("before-quit", () => setIsAppQuitting(true)); -} +}; + +main(); diff --git a/desktop/src/main/dialogs.ts b/desktop/src/main/dialogs.ts new file mode 100644 index 000000000..5f18878b5 --- /dev/null +++ b/desktop/src/main/dialogs.ts @@ -0,0 +1,54 @@ +import { dialog } from "electron/main"; +import path from "node:path"; +import { getDirFilePaths, getElectronFile } from "../services/fs"; +import { getElectronFilesFromGoogleZip } from "../services/upload"; +import type { ElectronFile } from "../types/ipc"; + +export const selectDirectory = async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + }); + if (result.filePaths && result.filePaths.length > 0) { + return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep); + } +}; + +export const showUploadFilesDialog = async () => { + const selectedFiles = await dialog.showOpenDialog({ + properties: ["openFile", "multiSelections"], + }); + const filePaths = selectedFiles.filePaths; + return await Promise.all(filePaths.map(getElectronFile)); +}; + +export const showUploadDirsDialog = async () => { + const dir = await dialog.showOpenDialog({ + properties: ["openDirectory", "multiSelections"], + }); + + let filePaths: string[] = []; + for (const dirPath of dir.filePaths) { + filePaths = [...filePaths, ...(await getDirFilePaths(dirPath))]; + } + + return await Promise.all(filePaths.map(getElectronFile)); +}; + +export const showUploadZipDialog = async () => { + const selectedFiles = await dialog.showOpenDialog({ + properties: ["openFile", "multiSelections"], + filters: [{ name: "Zip File", extensions: ["zip"] }], + }); + const filePaths = selectedFiles.filePaths; + + let files: ElectronFile[] = []; + + for (const filePath of filePaths) { + files = [...files, ...(await getElectronFilesFromGoogleZip(filePath))]; + } + + return { + zipPaths: filePaths, + files, + }; +}; diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts new file mode 100644 index 000000000..0da89fb00 --- /dev/null +++ b/desktop/src/main/fs.ts @@ -0,0 +1,133 @@ +/** + * @file file system related functions exposed over the context bridge. + */ +import { createWriteStream, existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Readable } from "node:stream"; + +export const fsExists = (path: string) => existsSync(path); + +/** + * Write a (web) ReadableStream to a file at the given {@link filePath}. + * + * The returned promise resolves when the write completes. + * + * @param filePath The local filesystem path where the file should be written. + * @param readableStream A [web + * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) + */ +export const writeStream = (filePath: string, readableStream: ReadableStream) => + writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream)); + +/** + * Convert a Web ReadableStream into a Node.js ReadableStream + * + * This can be used to, for example, write a ReadableStream obtained via + * `net.fetch` into a file using the Node.js `fs` APIs + */ +const convertWebReadableStreamToNode = (readableStream: ReadableStream) => { + const reader = readableStream.getReader(); + const rs = new Readable(); + + rs._read = async () => { + try { + const result = await reader.read(); + + if (!result.done) { + rs.push(Buffer.from(result.value)); + } else { + rs.push(null); + return; + } + } catch (e) { + rs.emit("error", e); + } + }; + + return rs; +}; + +const writeNodeStream = async ( + filePath: string, + fileStream: NodeJS.ReadableStream, +) => { + const writeable = createWriteStream(filePath); + + fileStream.on("error", (error) => { + writeable.destroy(error); // Close the writable stream with an error + }); + + fileStream.pipe(writeable); + + await new Promise((resolve, reject) => { + writeable.on("finish", resolve); + writeable.on("error", async (e: unknown) => { + if (existsSync(filePath)) { + await fs.unlink(filePath); + } + reject(e); + }); + }); +}; + +/* TODO: Audit below this */ + +export const checkExistsAndCreateDir = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); + +export const saveStreamToDisk = writeStream; + +export const saveFileToDisk = (path: string, contents: string) => + fs.writeFile(path, contents); + +export const readTextFile = async (filePath: string) => + fs.readFile(filePath, "utf-8"); + +export const moveFile = async (sourcePath: string, destinationPath: string) => { + if (!existsSync(sourcePath)) { + throw new Error("File does not exist"); + } + if (existsSync(destinationPath)) { + throw new Error("Destination file already exists"); + } + // check if destination folder exists + const destinationFolder = path.dirname(destinationPath); + await fs.mkdir(destinationFolder, { recursive: true }); + await fs.rename(sourcePath, destinationPath); +}; + +export const isFolder = async (dirPath: string) => { + if (!existsSync(dirPath)) return false; + const stats = await fs.stat(dirPath); + return stats.isDirectory(); +}; + +export const deleteFolder = async (folderPath: string) => { + // Ensure it is folder + if (!isFolder(folderPath)) return; + + // Ensure folder is empty + const files = await fs.readdir(folderPath); + if (files.length > 0) throw new Error("Folder is not empty"); + + // rm -rf it + await fs.rmdir(folderPath); +}; + +export const rename = async (oldPath: string, newPath: string) => { + if (!existsSync(oldPath)) throw new Error("Path does not exist"); + await fs.rename(oldPath, newPath); +}; + +export const deleteFile = async (filePath: string) => { + // Ensure it exists + if (!existsSync(filePath)) return; + + // And is a file + const stat = await fs.stat(filePath); + if (!stat.isFile()) throw new Error("Path is not a file"); + + // rm it + return fs.rm(filePath); +}; diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts new file mode 100644 index 000000000..0e94232aa --- /dev/null +++ b/desktop/src/main/init.ts @@ -0,0 +1,180 @@ +import { app, BrowserWindow, nativeImage, Tray } from "electron"; +import { existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { isAppQuitting, rendererURL } from "../main"; +import autoLauncher from "../services/autoLauncher"; +import { getHideDockIconPreference } from "../services/userPreference"; +import { isPlatform } from "../utils/common/platform"; +import log from "./log"; +import { createTrayContextMenu } from "./menu"; +import { isDev } from "./util"; + +/** + * Create an return the {@link BrowserWindow} that will form our app's UI. + * + * This window will show the HTML served from {@link rendererURL}. + */ +export const createWindow = async () => { + // Create the main window. This'll show our web content. + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(app.getAppPath(), "preload.js"), + }, + // The color to show in the window until the web content gets loaded. + // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property + backgroundColor: "black", + // We'll show it conditionally depending on `wasAutoLaunched` later. + show: false, + }); + + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + if (wasAutoLaunched) { + // Keep the macOS dock icon hidden if we were auto launched. + if (process.platform == "darwin") app.dock.hide(); + } else { + // Show our window (maximizing it) if this is not an auto-launch on + // login. + mainWindow.maximize(); + } + + mainWindow.loadURL(rendererURL); + + // Open the DevTools automatically when running in dev mode + if (isDev) mainWindow.webContents.openDevTools(); + + mainWindow.webContents.on("render-process-gone", (_, details) => { + log.error(`render-process-gone: ${details}`); + mainWindow.webContents.reload(); + }); + + mainWindow.webContents.on("unresponsive", () => { + log.error("webContents unresponsive"); + mainWindow.webContents.forcefullyCrashRenderer(); + }); + + mainWindow.on("close", function (event) { + if (!isAppQuitting()) { + event.preventDefault(); + mainWindow.hide(); + } + return false; + }); + + mainWindow.on("hide", () => { + // On macOS, also hide the app's icon in the dock if the user has + // selected the Settings > Hide dock icon checkbox. + const shouldHideDockIcon = getHideDockIconPreference(); + if (process.platform == "darwin" && shouldHideDockIcon) { + app.dock.hide(); + } + }); + + mainWindow.on("show", () => { + if (process.platform == "darwin") app.dock.show(); + }); + + return mainWindow; +}; + +export async function handleUpdates(mainWindow: BrowserWindow) {} + +export const setupTrayItem = (mainWindow: BrowserWindow) => { + const iconName = isPlatform("mac") + ? "taskbar-icon-Template.png" + : "taskbar-icon.png"; + const trayImgPath = path.join( + isDev ? "build" : process.resourcesPath, + iconName, + ); + const trayIcon = nativeImage.createFromPath(trayImgPath); + const tray = new Tray(trayIcon); + tray.setToolTip("ente"); + tray.setContextMenu(createTrayContextMenu(mainWindow)); +}; + +export function handleDownloads(mainWindow: BrowserWindow) { + mainWindow.webContents.session.on("will-download", (_, item) => { + item.setSavePath( + getUniqueSavePath(item.getFilename(), app.getPath("downloads")), + ); + }); +} + +export function handleExternalLinks(mainWindow: BrowserWindow) { + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (!url.startsWith(rendererURL)) { + require("electron").shell.openExternal(url); + return { action: "deny" }; + } else { + return { action: "allow" }; + } + }); +} + +export function getUniqueSavePath(filename: string, directory: string): string { + let uniqueFileSavePath = path.join(directory, filename); + const { name: filenameWithoutExtension, ext: extension } = + path.parse(filename); + let n = 0; + while (existsSync(uniqueFileSavePath)) { + n++; + // filter need to remove undefined extension from the array + // else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string + const fileNameWithNumberedSuffix = [ + `${filenameWithoutExtension}(${n})`, + extension, + ] + .filter((x) => x) // filters out undefined/null values + .join(""); + uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); + } + return uniqueFileSavePath; +} + +export function setupMacWindowOnDockIconClick() { + app.on("activate", function () { + const windows = BrowserWindow.getAllWindows(); + // we allow only one window + windows[0].show(); + }); +} + +export async function handleDockIconHideOnAutoLaunch() { + const shouldHideDockIcon = getHideDockIconPreference(); + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + + if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { + app.dock.hide(); + } +} + +export function logStartupBanner() { + const version = isDev ? "dev" : app.getVersion(); + log.info(`Hello from ente-photos-desktop ${version}`); + + const platform = process.platform; + const osRelease = os.release(); + const systemVersion = process.getSystemVersion(); + log.info("Running on", { platform, osRelease, systemVersion }); +} + +function lowerCaseHeaders(responseHeaders: Record) { + const headers: Record = {}; + for (const key of Object.keys(responseHeaders)) { + headers[key.toLowerCase()] = responseHeaders[key]; + } + return headers; +} + +export function addAllowOriginHeader(mainWindow: BrowserWindow) { + mainWindow.webContents.session.webRequest.onHeadersReceived( + (details, callback) => { + details.responseHeaders = lowerCaseHeaders(details.responseHeaders); + details.responseHeaders["access-control-allow-origin"] = ["*"]; + callback({ + responseHeaders: details.responseHeaders, + }); + }, + ); +} diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts new file mode 100644 index 000000000..be9798186 --- /dev/null +++ b/desktop/src/main/ipc.ts @@ -0,0 +1,268 @@ +/** + * @file Listen for IPC events sent/invoked by the renderer process, and route + * them to their correct handlers. + * + * This file is meant as a sibling to `preload.ts`, but this one runs in the + * context of the main process, and can import other files from `src/`. + * + * See [Note: types.ts <-> preload.ts <-> ipc.ts] + */ + +import type { FSWatcher } from "chokidar"; +import { ipcMain } from "electron/main"; +import { clearElectronStore } from "../api/electronStore"; +import { getEncryptionKey, setEncryptionKey } from "../api/safeStorage"; +import { + getElectronFilesFromGoogleZip, + getPendingUploads, + setToUploadCollection, + setToUploadFiles, +} from "../api/upload"; +import { + appVersion, + muteUpdateNotification, + skipAppUpdate, + updateAndRestart, +} from "../services/appUpdater"; +import { + computeImageEmbedding, + computeTextEmbedding, +} from "../services/clipService"; +import { runFFmpegCmd } from "../services/ffmpeg"; +import { getDirFiles } from "../services/fs"; +import { + convertToJPEG, + generateImageThumbnail, +} from "../services/imageProcessor"; +import { + addWatchMapping, + getWatchMappings, + removeWatchMapping, + updateWatchMappingIgnoredFiles, + updateWatchMappingSyncedFiles, +} from "../services/watch"; +import type { + ElectronFile, + FILE_PATH_TYPE, + Model, + WatchMapping, +} from "../types/ipc"; +import { + selectDirectory, + showUploadDirsDialog, + showUploadFilesDialog, + showUploadZipDialog, +} from "./dialogs"; +import { + checkExistsAndCreateDir, + deleteFile, + deleteFolder, + fsExists, + isFolder, + moveFile, + readTextFile, + rename, + saveFileToDisk, + saveStreamToDisk, +} from "./fs"; +import { logToDisk } from "./log"; +import { openDirectory, openLogDirectory } from "./util"; + +/** + * Listen for IPC events sent/invoked by the renderer process, and route them to + * their correct handlers. + */ +export const attachIPCHandlers = () => { + // Notes: + // + // The first parameter of the handler passed to `ipcMain.handle` is the + // `event`, and is usually ignored. The rest of the parameters are the + // arguments passed to `ipcRenderer.invoke`. + // + // [Note: Catching exception during .send/.on] + // + // While we can use ipcRenderer.send/ipcMain.on for one-way communication, + // that has the disadvantage that any exceptions thrown in the processing of + // the handler are not sent back to the renderer. So we use the + // ipcRenderer.invoke/ipcMain.handle 2-way pattern even for things that are + // conceptually one way. An exception (pun intended) to this is logToDisk, + // which is a primitive, frequently used, operation and shouldn't throw, so + // having its signature by synchronous is a bit convenient. + + // - General + + ipcMain.handle("appVersion", (_) => appVersion()); + + ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath)); + + ipcMain.handle("openLogDirectory", (_) => openLogDirectory()); + + // See [Note: Catching exception during .send/.on] + ipcMain.on("logToDisk", (_, message) => logToDisk(message)); + + ipcMain.on("clear-electron-store", (_) => { + clearElectronStore(); + }); + + ipcMain.handle("setEncryptionKey", (_, encryptionKey) => + setEncryptionKey(encryptionKey), + ); + + ipcMain.handle("getEncryptionKey", (_) => getEncryptionKey()); + + // - App update + + ipcMain.on("update-and-restart", (_) => updateAndRestart()); + + ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version)); + + ipcMain.on("mute-update-notification", (_, version) => + muteUpdateNotification(version), + ); + + // - Conversion + + ipcMain.handle("convertToJPEG", (_, fileData, filename) => + convertToJPEG(fileData, filename), + ); + + ipcMain.handle( + "generateImageThumbnail", + (_, inputFile, maxDimension, maxSize) => + generateImageThumbnail(inputFile, maxDimension, maxSize), + ); + + ipcMain.handle( + "runFFmpegCmd", + ( + _, + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, + ) => runFFmpegCmd(cmd, inputFile, outputFileName, dontTimeout), + ); + + // - ML + + ipcMain.handle( + "computeImageEmbedding", + (_, model: Model, imageData: Uint8Array) => + computeImageEmbedding(model, imageData), + ); + + ipcMain.handle("computeTextEmbedding", (_, model: Model, text: string) => + computeTextEmbedding(model, text), + ); + + // - File selection + + ipcMain.handle("selectDirectory", (_) => selectDirectory()); + + ipcMain.handle("showUploadFilesDialog", (_) => showUploadFilesDialog()); + + ipcMain.handle("showUploadDirsDialog", (_) => showUploadDirsDialog()); + + ipcMain.handle("showUploadZipDialog", (_) => showUploadZipDialog()); + + // - FS + + ipcMain.handle("fsExists", (_, path) => fsExists(path)); + + // - FS Legacy + + ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) => + checkExistsAndCreateDir(dirPath), + ); + + ipcMain.handle( + "saveStreamToDisk", + (_, path: string, fileStream: ReadableStream) => + saveStreamToDisk(path, fileStream), + ); + + ipcMain.handle("saveFileToDisk", (_, path: string, file: any) => + saveFileToDisk(path, file), + ); + + ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path)); + + ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath)); + + ipcMain.handle("moveFile", (_, oldPath: string, newPath: string) => + moveFile(oldPath, newPath), + ); + + ipcMain.handle("deleteFolder", (_, path: string) => deleteFolder(path)); + + ipcMain.handle("deleteFile", (_, path: string) => deleteFile(path)); + + ipcMain.handle("rename", (_, oldPath: string, newPath: string) => + rename(oldPath, newPath), + ); + + // - Upload + + ipcMain.handle("getPendingUploads", (_) => getPendingUploads()); + + ipcMain.handle( + "setToUploadFiles", + (_, type: FILE_PATH_TYPE, filePaths: string[]) => + setToUploadFiles(type, filePaths), + ); + + ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) => + getElectronFilesFromGoogleZip(filePath), + ); + + ipcMain.handle("setToUploadCollection", (_, collectionName: string) => + setToUploadCollection(collectionName), + ); + + ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath)); +}; + +/** + * Sibling of {@link attachIPCHandlers} that attaches handlers specific to the + * watch folder functionality. + * + * It gets passed a {@link FSWatcher} instance which it can then forward to the + * actual handlers. + */ +export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => { + // - Watch + + ipcMain.handle( + "addWatchMapping", + ( + _, + collectionName: string, + folderPath: string, + uploadStrategy: number, + ) => + addWatchMapping( + watcher, + collectionName, + folderPath, + uploadStrategy, + ), + ); + + ipcMain.handle("removeWatchMapping", (_, folderPath: string) => + removeWatchMapping(watcher, folderPath), + ); + + ipcMain.handle("getWatchMappings", (_) => getWatchMappings()); + + ipcMain.handle( + "updateWatchMappingSyncedFiles", + (_, folderPath: string, files: WatchMapping["syncedFiles"]) => + updateWatchMappingSyncedFiles(folderPath, files), + ); + + ipcMain.handle( + "updateWatchMappingIgnoredFiles", + (_, folderPath: string, files: WatchMapping["ignoredFiles"]) => + updateWatchMappingIgnoredFiles(folderPath, files), + ); +}; diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts new file mode 100644 index 000000000..8787a530d --- /dev/null +++ b/desktop/src/main/log.ts @@ -0,0 +1,131 @@ +import log from "electron-log"; +import util from "node:util"; +import { isDev } from "./util"; + +/** + * Initialize logging in the main process. + * + * This will set our underlying logger up to log to a file named `ente.log`, + * + * - on Linux at ~/.config/ente/logs/main.log + * - on macOS at ~/Library/Logs/ente/main.log + * - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log + * + * On dev builds, it will also log to the console. + */ +export const initLogging = () => { + log.transports.file.fileName = "ente.log"; + log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; + log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}"; + + log.transports.console.level = false; +}; + +/** + * Write a {@link message} to the on-disk log. + * + * This is used by the renderer process (via the contextBridge) to add entries + * in the log that is saved on disk. + */ +export const logToDisk = (message: string) => { + log.info(`[rndr] ${message}`); +}; + +export const logError = logErrorSentry; + +/** Deprecated, but no alternative yet */ +export function logErrorSentry( + error: any, + msg: string, + info?: Record, +) { + logToDisk( + `error: ${error?.name} ${error?.message} ${ + error?.stack + } msg: ${msg} info: ${JSON.stringify(info)}`, + ); + if (isDev) { + console.log(error, { msg, info }); + } +} + +const logError1 = (message: string, e?: unknown) => { + if (!e) { + logError_(message); + return; + } + + 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}\n${e.stack}`; + } else { + // For the rest rare cases, use the default string serialization of e. + es = String(e); + } + + logError_(`${message}: ${es}`); +}; + +const logError_ = (message: string) => { + log.error(`[main] [error] ${message}`); + if (isDev) console.error(`[error] ${message}`); +}; + +const logInfo = (...params: any[]) => { + const message = params + .map((p) => (typeof p == "string" ? p : util.inspect(p))) + .join(" "); + log.info(`[main] ${message}`); + if (isDev) console.log(message); +}; + +const logDebug = (param: () => any) => { + if (isDev) console.log(`[debug] ${util.inspect(param())}`); +}; + +/** + * Ente's logger. + * + * This is an object that provides three functions to log at the corresponding + * levels - error, info or debug. + * + * {@link initLogging} needs to be called once before using any of these. + */ +export default { + /** + * Log an error message with an optional associated error object. + * + * {@link e} is generally expected to be an `instanceof Error` but it can be + * any arbitrary object that we obtain, say, when in a try-catch handler. + * + * The log is written to disk. In development builds, the log is also + * printed to the (Node.js process') console. + */ + error: logError1, + /** + * Log a message. + * + * This is meant as a replacement of {@link console.log}, and takes an + * arbitrary number of arbitrary parameters that it then serializes. + * + * The log is written to disk. In development builds, the log is also + * printed to the (Node.js process') console. + */ + info: logInfo, + /** + * Log a debug message. + * + * To avoid running unnecessary code in release builds, this takes a + * function to call to get the log message instead of directly taking the + * message. The provided function will only be called in development builds. + * + * The function can return an arbitrary value which is serialied before + * being logged. + * + * This log is not written to disk. It is printed to the (Node.js process') + * console only on development builds. + */ + debug: logDebug, +}; diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts new file mode 100644 index 000000000..658932961 --- /dev/null +++ b/desktop/src/main/menu.ts @@ -0,0 +1,214 @@ +import { + app, + BrowserWindow, + Menu, + MenuItemConstructorOptions, + shell, +} from "electron"; +import { setIsAppQuitting } from "../main"; +import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; +import autoLauncher from "../services/autoLauncher"; +import { + getHideDockIconPreference, + setHideDockIconPreference, +} from "../services/userPreference"; +import { openLogDirectory } from "./util"; + +/** Create and return the entries in the app's main menu bar */ +export const createApplicationMenu = async (mainWindow: BrowserWindow) => { + // The state of checkboxes + // + // Whenever the menu is redrawn the current value of these variables is used + // to set the checked state for the various settings checkboxes. + let isAutoLaunchEnabled = await autoLauncher.isEnabled(); + let shouldHideDockIcon = getHideDockIconPreference(); + + const macOSOnly = (options: MenuItemConstructorOptions[]) => + process.platform == "darwin" ? options : []; + + const handleCheckForUpdates = () => + forceCheckForUpdateAndNotify(mainWindow); + + const handleViewChangelog = () => + shell.openExternal( + "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", + ); + + const toggleAutoLaunch = () => { + autoLauncher.toggleAutoLaunch(); + isAutoLaunchEnabled = !isAutoLaunchEnabled; + }; + + const toggleHideDockIcon = () => { + setHideDockIconPreference(!shouldHideDockIcon); + shouldHideDockIcon = !shouldHideDockIcon; + }; + + const handleHelp = () => shell.openExternal("https://help.ente.io/photos/"); + + const handleSupport = () => shell.openExternal("mailto:support@ente.io"); + + const handleBlog = () => shell.openExternal("https://ente.io/blog/"); + + const handleViewLogs = openLogDirectory; + + return Menu.buildFromTemplate([ + { + label: "ente", + submenu: [ + ...macOSOnly([ + { + label: "About Ente", + role: "about", + }, + ]), + { type: "separator" }, + { + label: "Check for Updates...", + click: handleCheckForUpdates, + }, + { + label: "View Changelog", + click: handleViewChangelog, + }, + { type: "separator" }, + + { + label: "Preferences", + submenu: [ + { + label: "Open Ente on Startup", + type: "checkbox", + checked: isAutoLaunchEnabled, + click: toggleAutoLaunch, + }, + { + label: "Hide Dock Icon", + type: "checkbox", + checked: shouldHideDockIcon, + click: toggleHideDockIcon, + }, + ], + }, + + { type: "separator" }, + ...macOSOnly([ + { + label: "Hide Ente", + role: "hide", + }, + { + label: "Hide Others", + role: "hideOthers", + }, + { type: "separator" }, + ]), + { + label: "Quit", + role: "quit", + }, + ], + }, + { + label: "Edit", + submenu: [ + { label: "Undo", role: "undo" }, + { label: "Redo", role: "redo" }, + { type: "separator" }, + { label: "Cut", role: "cut" }, + { label: "Copy", role: "copy" }, + { label: "Paste", role: "paste" }, + { label: "Select All", role: "selectAll" }, + ...macOSOnly([ + { type: "separator" }, + { + label: "Speech", + submenu: [ + { + role: "startSpeaking", + label: "start speaking", + }, + { + role: "stopSpeaking", + label: "stop speaking", + }, + ], + }, + ]), + ], + }, + { + label: "View", + submenu: [ + { label: "Reload", role: "reload" }, + { label: "Toggle Dev Tools", role: "toggleDevTools" }, + { type: "separator" }, + { label: "Toggle Full Screen", role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { label: "Minimize", role: "minimize" }, + { label: "Zoom", role: "zoom" }, + { label: "Close", role: "close" }, + ...macOSOnly([ + { type: "separator" }, + { label: "Bring All to Front", role: "front" }, + { type: "separator" }, + { label: "Ente", role: "window" }, + ]), + ], + }, + { + label: "Help", + submenu: [ + { + label: "Ente Help", + click: handleHelp, + }, + { type: "separator" }, + { + label: "Support", + click: handleSupport, + }, + { + label: "Product Updates", + click: handleBlog, + }, + { type: "separator" }, + { + label: "View Logs", + click: handleViewLogs, + }, + ], + }, + ]); +}; + +/** + * Create and return a {@link Menu} that is shown when the user clicks on our + * system tray icon (e.g. the icon list at the top right of the screen on macOS) + */ +export const createTrayContextMenu = (mainWindow: BrowserWindow) => { + const handleOpen = () => { + mainWindow.maximize(); + mainWindow.show(); + }; + + const handleClose = () => { + setIsAppQuitting(true); + app.quit(); + }; + + return Menu.buildFromTemplate([ + { + label: "Open Ente", + click: handleOpen, + }, + { + label: "Quit Ente", + click: handleClose, + }, + ]); +}; diff --git a/desktop/src/main/util.ts b/desktop/src/main/util.ts new file mode 100644 index 000000000..d0c6699e9 --- /dev/null +++ b/desktop/src/main/util.ts @@ -0,0 +1,81 @@ +import shellescape from "any-shell-escape"; +import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */ +import { app } from "electron/main"; +import { exec } from "node:child_process"; +import path from "node:path"; +import { promisify } from "node:util"; +import log from "./log"; + +/** `true` if the app is running in development mode. */ +export const isDev = !app.isPackaged; + +/** + * Run a shell command asynchronously. + * + * This is a convenience promisified version of child_process.exec. It runs the + * command asynchronously and returns its stdout and stderr if there were no + * errors. + * + * If the command is passed as a string, then it will be executed verbatim. + * + * If the command is passed as an array, then the first argument will be treated + * as the executable and the remaining (optional) items as the command line + * parameters. This function will shellescape and join the array to form the + * command that finally gets executed. + * + * > Note: This is not a 1-1 replacement of child_process.exec - if you're + * > trying to run a trivial shell command, say something that produces a lot of + * > output, this might not be the best option and it might be better to use the + * > underlying functions. + */ +export const execAsync = (command: string | string[]) => { + const escapedCommand = Array.isArray(command) + ? shellescape(command) + : command; + const startTime = Date.now(); + log.debug(() => `Running shell command: ${escapedCommand}`); + const result = execAsync_(escapedCommand); + log.debug( + () => + `Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`, + ); + return result; +}; + +const execAsync_ = promisify(exec); + +/** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ +export const openDirectory = async (dirPath: string) => { + const res = await shell.openPath(path.normalize(dirPath)); + // shell.openPath resolves with a string containing the error message + // corresponding to the failure if a failure occurred, otherwise "". + if (res) throw new Error(`Failed to open directory ${dirPath}: res`); +}; + +/** + * Return the path where the logs for the app are saved. + * + * [Note: Electron app paths] + * + * By default, these paths are at the following locations: + * + * - macOS: `~/Library/Application Support/ente` + * - Linux: `~/.config/ente` + * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` + * - Windows: C:\Users\\AppData\Local\ + * + * https://www.electronjs.org/docs/latest/api/app + * + */ +const logDirectoryPath = () => app.getPath("logs"); + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +export const openLogDirectory = () => openDirectory(logDirectoryPath()); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index a602e76bb..4b171e28e 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -1,123 +1,377 @@ -import { - deleteDiskCache, - getCacheDirectory, - openDiskCache, - setCustomCacheDirectory, -} from "./api/cache"; -import { computeImageEmbedding, computeTextEmbedding } from "./api/clip"; -import { - getAppVersion, - getPlatform, - logToDisk, +/** + * @file The preload script + * + * The preload script runs in a renderer process before its web contents begin + * loading. During their execution they have access to a subset of Node.js APIs + * and imports. Its purpose is to expose the relevant imports and other + * functions as an object on the DOM, so that the renderer process can invoke + * functions that live in the main (Node.js) process if needed. + * + * Ref: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload + * + * Note that this script cannot import other code from `src/` - conceptually it + * can be thought of as running in a separate, third, process different from + * both the main or a renderer process (technically, it runs in a BrowserWindow + * context that runs prior to the renderer process). + * + * > Since enabling the sandbox disables Node.js integration in your preload + * > scripts, you can no longer use require("../my-script"). In other words, + * > your preload script needs to be a single file. + * > + * > https://www.electronjs.org/blog/breach-to-barrier + * + * If we really wanted, we could setup a bundler to package this into a single + * file. However, since this is just boilerplate code providing a bridge between + * the main and renderer, we avoid introducing another moving part into the mix + * and just keep the entire preload setup in this single file. + * + * [Note: types.ts <-> preload.ts <-> ipc.ts] + * + * The following three files are boilerplatish linkage of the same functions, + * and when changing one of them, remember to see if the other two also need + * changing: + * + * - [renderer] web/packages/shared/electron/types.ts contains docs + * - [preload] desktop/src/preload.ts ↕︎ + * - [main] desktop/src/main/ipc.ts contains impl + */ + +import { contextBridge, ipcRenderer } from "electron/renderer"; + +// While we can't import other code, we can import types since they're just +// needed when compiling and will not be needed / looked around for at runtime. +import type { + AppUpdateInfo, + ElectronFile, + FILE_PATH_TYPE, + Model, + WatchMapping, +} from "./types/ipc"; + +// - General + +const appVersion = (): Promise => ipcRenderer.invoke("appVersion"); + +const openDirectory = (dirPath: string): Promise => + ipcRenderer.invoke("openDirectory"); + +const openLogDirectory = (): Promise => + ipcRenderer.invoke("openLogDirectory"); + +const logToDisk = (message: string): void => + ipcRenderer.send("logToDisk", message); + +const fsExists = (path: string): Promise => + ipcRenderer.invoke("fsExists", path); + +// - AUDIT below this + +const registerForegroundEventListener = (onForeground: () => void) => { + ipcRenderer.removeAllListeners("app-in-foreground"); + ipcRenderer.on("app-in-foreground", () => { + onForeground(); + }); +}; + +const clearElectronStore = () => { + ipcRenderer.send("clear-electron-store"); +}; + +const setEncryptionKey = (encryptionKey: string): Promise => + ipcRenderer.invoke("setEncryptionKey", encryptionKey); + +const getEncryptionKey = (): Promise => + ipcRenderer.invoke("getEncryptionKey"); + +// - App update + +const registerUpdateEventListener = ( + showUpdateDialog: (updateInfo: AppUpdateInfo) => void, +) => { + ipcRenderer.removeAllListeners("show-update-dialog"); + ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => { + showUpdateDialog(updateInfo); + }); +}; + +const updateAndRestart = () => { + ipcRenderer.send("update-and-restart"); +}; + +const skipAppUpdate = (version: string) => { + ipcRenderer.send("skip-app-update", version); +}; + +const muteUpdateNotification = (version: string) => { + ipcRenderer.send("mute-update-notification", version); +}; + +// - Conversion + +const convertToJPEG = ( + fileData: Uint8Array, + filename: string, +): Promise => + ipcRenderer.invoke("convertToJPEG", fileData, filename); + +const generateImageThumbnail = ( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number, +): Promise => + ipcRenderer.invoke( + "generateImageThumbnail", + inputFile, + maxDimension, + maxSize, + ); + +const runFFmpegCmd = ( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, +): Promise => + ipcRenderer.invoke( + "runFFmpegCmd", + cmd, + inputFile, + outputFileName, + dontTimeout, + ); + +// - ML + +const computeImageEmbedding = ( + model: Model, + imageData: Uint8Array, +): Promise => + ipcRenderer.invoke("computeImageEmbedding", model, imageData); + +const computeTextEmbedding = ( + model: Model, + text: string, +): Promise => + ipcRenderer.invoke("computeTextEmbedding", model, text); + +// - File selection + +// TODO: Deprecated - use dialogs on the renderer process itself + +const selectDirectory = (): Promise => + ipcRenderer.invoke("selectDirectory"); + +const showUploadFilesDialog = (): Promise => + ipcRenderer.invoke("showUploadFilesDialog"); + +const showUploadDirsDialog = (): Promise => + ipcRenderer.invoke("showUploadDirsDialog"); + +const showUploadZipDialog = (): Promise<{ + zipPaths: string[]; + files: ElectronFile[]; +}> => ipcRenderer.invoke("showUploadZipDialog"); + +// - Watch + +const registerWatcherFunctions = ( + addFile: (file: ElectronFile) => Promise, + removeFile: (path: string) => Promise, + removeFolder: (folderPath: string) => Promise, +) => { + ipcRenderer.removeAllListeners("watch-add"); + ipcRenderer.removeAllListeners("watch-unlink"); + ipcRenderer.removeAllListeners("watch-unlink-dir"); + ipcRenderer.on("watch-add", (_, file: ElectronFile) => addFile(file)); + ipcRenderer.on("watch-unlink", (_, filePath: string) => + removeFile(filePath), + ); + ipcRenderer.on("watch-unlink-dir", (_, folderPath: string) => + removeFolder(folderPath), + ); +}; + +const addWatchMapping = ( + collectionName: string, + folderPath: string, + uploadStrategy: number, +): Promise => + ipcRenderer.invoke( + "addWatchMapping", + collectionName, + folderPath, + uploadStrategy, + ); + +const removeWatchMapping = (folderPath: string): Promise => + ipcRenderer.invoke("removeWatchMapping", folderPath); + +const getWatchMappings = (): Promise => + ipcRenderer.invoke("getWatchMappings"); + +const updateWatchMappingSyncedFiles = ( + folderPath: string, + files: WatchMapping["syncedFiles"], +): Promise => + ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files); + +const updateWatchMappingIgnoredFiles = ( + folderPath: string, + files: WatchMapping["ignoredFiles"], +): Promise => + ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files); + +// - FS Legacy + +const checkExistsAndCreateDir = (dirPath: string): Promise => + ipcRenderer.invoke("checkExistsAndCreateDir", dirPath); + +const saveStreamToDisk = ( + path: string, + fileStream: ReadableStream, +): Promise => ipcRenderer.invoke("saveStreamToDisk", path, fileStream); + +const saveFileToDisk = (path: string, file: any): Promise => + ipcRenderer.invoke("saveFileToDisk", path, file); + +const readTextFile = (path: string): Promise => + ipcRenderer.invoke("readTextFile", path); + +const isFolder = (dirPath: string): Promise => + ipcRenderer.invoke("isFolder", dirPath); + +const moveFile = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("moveFile", oldPath, newPath); + +const deleteFolder = (path: string): Promise => + ipcRenderer.invoke("deleteFolder", path); + +const deleteFile = (path: string): Promise => + ipcRenderer.invoke("deleteFile", path); + +const rename = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("rename", oldPath, newPath); + +// - Upload + +const getPendingUploads = (): Promise<{ + files: ElectronFile[]; + collectionName: string; + type: string; +}> => ipcRenderer.invoke("getPendingUploads"); + +const setToUploadFiles = ( + type: FILE_PATH_TYPE, + filePaths: string[], +): Promise => ipcRenderer.invoke("setToUploadFiles", type, filePaths); + +const getElectronFilesFromGoogleZip = ( + filePath: string, +): Promise => + ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath); + +const setToUploadCollection = (collectionName: string): Promise => + ipcRenderer.invoke("setToUploadCollection", collectionName); + +const getDirFiles = (dirPath: string): Promise => + ipcRenderer.invoke("getDirFiles", dirPath); + +// These objects exposed here will become available to the JS code in our +// renderer (the web/ code) as `window.ElectronAPIs.*` +// +// There are a few related concepts at play here, and it might be worthwhile to +// read their (excellent) documentation to get an understanding; +//` +// - ContextIsolation: +// https://www.electronjs.org/docs/latest/tutorial/context-isolation +// +// - IPC https://www.electronjs.org/docs/latest/tutorial/ipc +// +// [Note: Transferring large amount of data over IPC] +// +// Electron's IPC implementation uses the HTML standard Structured Clone +// Algorithm to serialize objects passed between processes. +// https://www.electronjs.org/docs/latest/tutorial/ipc#object-serialization +// +// In particular, ArrayBuffer is eligible for structured cloning. +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +// +// Also, ArrayBuffer is "transferable", which means it is a zero-copy operation +// operation when it happens across threads. +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects +// +// In our case though, we're not dealing with threads but separate processes. So +// the ArrayBuffer will be copied: +// > "parameters, errors and return values are **copied** when they're sent over +// the bridge". +// https://www.electronjs.org/docs/latest/api/context-bridge#methods +// +// The copy itself is relatively fast, but the problem with transfering large +// amounts of data is potentially running out of memory during the copy. +contextBridge.exposeInMainWorld("ElectronAPIs", { + // - General + appVersion, openDirectory, - openLogDirectory, - selectDirectory, -} from "./api/common"; -import { clearElectronStore } from "./api/electronStore"; -import { - checkExistsAndCreateDir, - exists, - saveFileToDisk, - saveStreamToDisk, -} from "./api/export"; -import { runFFmpegCmd } from "./api/ffmpeg"; -import { - deleteFile, - deleteFolder, - getDirFiles, - isFolder, - moveFile, - readTextFile, - rename, -} from "./api/fs"; -import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor"; -import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage"; -import { - muteUpdateNotification, registerForegroundEventListener, - registerUpdateEventListener, - reloadWindow, - sendNotification, - skipAppUpdate, - updateAndRestart, -} from "./api/system"; -import { - getElectronFilesFromGoogleZip, - getPendingUploads, - setToUploadCollection, - setToUploadFiles, - showUploadDirsDialog, - showUploadFilesDialog, - showUploadZipDialog, -} from "./api/upload"; -import { - addWatchMapping, - getWatchMappings, - registerWatcherFunctions, - removeWatchMapping, - updateWatchMappingIgnoredFiles, - updateWatchMappingSyncedFiles, -} from "./api/watch"; -import { setupLogging } from "./utils/logging"; -import { - logRendererProcessMemoryUsage, - setupRendererProcessStatsLogger, -} from "./utils/processStats"; - -setupLogging(); -setupRendererProcessStatsLogger(); - -const windowObject: any = window; - -windowObject["ElectronAPIs"] = { - exists, - checkExistsAndCreateDir, - saveStreamToDisk, - saveFileToDisk, - selectDirectory, clearElectronStore, - sendNotification, - reloadWindow, - readTextFile, - showUploadFilesDialog, - showUploadDirsDialog, - getPendingUploads, - setToUploadFiles, - showUploadZipDialog, - getElectronFilesFromGoogleZip, - setToUploadCollection, getEncryptionKey, setEncryptionKey, - openDiskCache, - deleteDiskCache, - getDirFiles, - getWatchMappings, - addWatchMapping, - removeWatchMapping, - registerWatcherFunctions, - isFolder, - updateWatchMappingSyncedFiles, - updateWatchMappingIgnoredFiles, - logToDisk, - convertToJPEG, + + // - Logging openLogDirectory, - registerUpdateEventListener, + logToDisk, + + // - App update updateAndRestart, skipAppUpdate, - getAppVersion, - runFFmpegCmd, muteUpdateNotification, + registerUpdateEventListener, + + // - Conversion + convertToJPEG, generateImageThumbnail, - logRendererProcessMemoryUsage, - registerForegroundEventListener, - openDirectory, - moveFile, - deleteFolder, - rename, - deleteFile, + runFFmpegCmd, + + // - ML computeImageEmbedding, computeTextEmbedding, - getPlatform, - getCacheDirectory, - setCustomCacheDirectory, -}; + + // - File selection + selectDirectory, + showUploadFilesDialog, + showUploadDirsDialog, + showUploadZipDialog, + + // - Watch + registerWatcherFunctions, + addWatchMapping, + removeWatchMapping, + getWatchMappings, + updateWatchMappingSyncedFiles, + updateWatchMappingIgnoredFiles, + + // - FS + fs: { + exists: fsExists, + }, + + // - FS legacy + // TODO: Move these into fs + document + rename if needed + checkExistsAndCreateDir, + saveStreamToDisk, + saveFileToDisk, + readTextFile, + isFolder, + moveFile, + deleteFolder, + deleteFile, + rename, + + // - Upload + + getPendingUploads, + setToUploadFiles, + getElectronFilesFromGoogleZip, + setToUploadCollection, + getDirFiles, +}); diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts index 2ddcef704..98db606a4 100644 --- a/desktop/src/services/appUpdater.ts +++ b/desktop/src/services/appUpdater.ts @@ -2,11 +2,9 @@ import { compareVersions } from "compare-versions"; import { app, BrowserWindow } from "electron"; import { default as ElectronLog, default as log } from "electron-log"; import { autoUpdater } from "electron-updater"; -import fetch from "node-fetch"; import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; -import { AppUpdateInfo, GetFeatureFlagResponse } from "../types"; -import { isPlatform } from "../utils/common/platform"; -import { logErrorSentry } from "./sentry"; +import { logErrorSentry } from "../main/log"; +import { AppUpdateInfo } from "../types/ipc"; import { clearMuteUpdateNotificationVersion, clearSkipAppVersion, @@ -64,56 +62,42 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { ); return; } - const desktopCutoffVersion = await getDesktopCutoffVersion(); + + let timeout: NodeJS.Timeout; + log.debug("attempting auto update"); + autoUpdater.downloadUpdate(); + const muteUpdateNotificationVersion = + getMuteUpdateNotificationVersion(); if ( - desktopCutoffVersion && - isPlatform("mac") && - compareVersions( - updateCheckResult.updateInfo.version, - desktopCutoffVersion, - ) > 0 + muteUpdateNotificationVersion && + updateCheckResult.updateInfo.version === + muteUpdateNotificationVersion ) { - log.debug("auto update not possible due to key change"); + log.info( + "user chose to mute update notification for version ", + updateCheckResult.updateInfo.version, + ); + return; + } + autoUpdater.on("update-downloaded", () => { + timeout = setTimeout( + () => + showUpdateDialog(mainWindow, { + autoUpdatable: true, + version: updateCheckResult.updateInfo.version, + }), + FIVE_MIN_IN_MICROSECOND, + ); + }); + autoUpdater.on("error", (error) => { + clearTimeout(timeout); + logErrorSentry(error, "auto update failed"); showUpdateDialog(mainWindow, { autoUpdatable: false, version: updateCheckResult.updateInfo.version, }); - } else { - let timeout: NodeJS.Timeout; - log.debug("attempting auto update"); - autoUpdater.downloadUpdate(); - const muteUpdateNotificationVersion = - getMuteUpdateNotificationVersion(); - if ( - muteUpdateNotificationVersion && - updateCheckResult.updateInfo.version === - muteUpdateNotificationVersion - ) { - log.info( - "user chose to mute update notification for version ", - updateCheckResult.updateInfo.version, - ); - return; - } - autoUpdater.on("update-downloaded", () => { - timeout = setTimeout( - () => - showUpdateDialog(mainWindow, { - autoUpdatable: true, - version: updateCheckResult.updateInfo.version, - }), - FIVE_MIN_IN_MICROSECOND, - ); - }); - autoUpdater.on("error", (error) => { - clearTimeout(timeout); - logErrorSentry(error, "auto update failed"); - showUpdateDialog(mainWindow, { - autoUpdatable: false, - version: updateCheckResult.updateInfo.version, - }); - }); - } + }); + setIsUpdateAvailable(true); } catch (e) { logErrorSentry(e, "checkForUpdateAndNotify failed"); @@ -126,9 +110,12 @@ export function updateAndRestart() { autoUpdater.quitAndInstall(); } -export function getAppVersion() { - return `v${app.getVersion()}`; -} +/** + * Return the version of the desktop app + * + * The return value is of the form `v1.2.3`. + */ +export const appVersion = () => `v${app.getVersion()}`; export function skipAppUpdate(version: string) { setSkipAppVersion(version); @@ -138,18 +125,6 @@ export function muteUpdateNotification(version: string) { setMuteUpdateNotificationVersion(version); } -async function getDesktopCutoffVersion() { - try { - const featureFlags = ( - await fetch("https://static.ente.io/feature_flags.json") - ).json() as GetFeatureFlagResponse; - return featureFlags.desktopCutoffVersion; - } catch (e) { - logErrorSentry(e, "failed to get feature flags"); - return undefined; - } -} - function showUpdateDialog( mainWindow: BrowserWindow, updateInfo: AppUpdateInfo, diff --git a/desktop/src/services/autoLauncher.ts b/desktop/src/services/autoLauncher.ts index 5cac556a9..bc1270ac9 100644 --- a/desktop/src/services/autoLauncher.ts +++ b/desktop/src/services/autoLauncher.ts @@ -1,4 +1,4 @@ -import { AutoLauncherClient } from "../types/autoLauncher"; +import { AutoLauncherClient } from "../types/main"; import { isPlatform } from "../utils/common/platform"; import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher"; import macAutoLauncher from "./autoLauncherClients/macAutoLauncher"; diff --git a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts index 132f8d1f5..761b58a06 100644 --- a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts +++ b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts @@ -1,6 +1,6 @@ import AutoLaunch from "auto-launch"; import { app } from "electron"; -import { AutoLauncherClient } from "../../types/autoLauncher"; +import { AutoLauncherClient } from "../../types/main"; const LAUNCHED_AS_HIDDEN_FLAG = "hidden"; diff --git a/desktop/src/services/autoLauncherClients/macAutoLauncher.ts b/desktop/src/services/autoLauncherClients/macAutoLauncher.ts index fcdc7bd81..d4fc343b0 100644 --- a/desktop/src/services/autoLauncherClients/macAutoLauncher.ts +++ b/desktop/src/services/autoLauncherClients/macAutoLauncher.ts @@ -1,5 +1,5 @@ import { app } from "electron"; -import { AutoLauncherClient } from "../../types/autoLauncher"; +import { AutoLauncherClient } from "../../types/main"; class MacAutoLauncher implements AutoLauncherClient { async isEnabled() { diff --git a/desktop/src/services/chokidar.ts b/desktop/src/services/chokidar.ts index f0d217d09..57a0e504e 100644 --- a/desktop/src/services/chokidar.ts +++ b/desktop/src/services/chokidar.ts @@ -1,7 +1,16 @@ import chokidar from "chokidar"; import { BrowserWindow } from "electron"; -import { getWatchMappings } from "../api/watch"; -import { logError } from "../services/logging"; +import path from "path"; +import { logError } from "../main/log"; +import { getWatchMappings } from "../services/watch"; +import { getElectronFile } from "./fs"; + +/** + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. + */ +const normalizeToPOSIX = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); export function initWatcher(mainWindow: BrowserWindow) { const mappings = getWatchMappings(); @@ -13,17 +22,20 @@ export function initWatcher(mainWindow: BrowserWindow) { awaitWriteFinish: true, }); watcher - .on("add", (path) => { - mainWindow.webContents.send("watch-add", path); - }) - .on("change", (path) => { - mainWindow.webContents.send("watch-change", path); + .on("add", async (path) => { + mainWindow.webContents.send( + "watch-add", + await getElectronFile(normalizeToPOSIX(path)), + ); }) .on("unlink", (path) => { - mainWindow.webContents.send("watch-unlink", path); + mainWindow.webContents.send("watch-unlink", normalizeToPOSIX(path)); }) .on("unlinkDir", (path) => { - mainWindow.webContents.send("watch-unlink-dir", path); + mainWindow.webContents.send( + "watch-unlink-dir", + normalizeToPOSIX(path), + ); }) .on("error", (error) => { logError(error, "error while watching files"); diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index 4a808d7a4..41e559a9b 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -1,20 +1,16 @@ -import { app } from "electron"; -import * as log from "electron-log"; +import { app, net } from "electron/main"; import { existsSync } from "fs"; -import fs from "fs/promises"; -import fetch from "node-fetch"; -import path from "path"; -import { readFile } from "promise-fs"; -import util from "util"; +import fs from "node:fs/promises"; +import path from "node:path"; import { CustomErrors } from "../constants/errors"; -import { Model } from "../types"; +import { writeStream } from "../main/fs"; +import log, { logErrorSentry } from "../main/log"; +import { execAsync, isDev } from "../main/util"; +import { Model } from "../types/ipc"; import Tokenizer from "../utils/clip-bpe-ts/mod"; -import { isDev } from "../utils/common"; import { getPlatform } from "../utils/common/platform"; -import { writeNodeStream } from "./fs"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); -const execAsync = util.promisify(require("child_process").exec); +import { generateTempFilePath } from "../utils/temp"; +import { deleteTempFile } from "./ffmpeg"; const jpeg = require("jpeg-js"); const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL"; @@ -65,28 +61,18 @@ const TEXT_MODEL_SIZE_IN_BYTES = { onnx: 64173509, // 61.2 MB }; -const MODEL_SAVE_FOLDER = "models"; - -function getModelSavePath(modelName: string) { - let userDataDir: string; - if (isDev) { - userDataDir = "."; - } else { - userDataDir = app.getPath("userData"); - } - return path.join(userDataDir, MODEL_SAVE_FOLDER, modelName); -} +/** Return the path where the given {@link modelName} is meant to be saved */ +const getModelSavePath = (modelName: string) => + path.join(app.getPath("userData"), "models", modelName); async function downloadModel(saveLocation: string, url: string) { // confirm that the save location exists const saveDir = path.dirname(saveLocation); - if (!existsSync(saveDir)) { - log.info("creating model save dir"); - await fs.mkdir(saveDir, { recursive: true }); - } + await fs.mkdir(saveDir, { recursive: true }); log.info("downloading clip model"); - const resp = await fetch(url); - await writeNodeStream(saveLocation, resp.body); + const res = await net.fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + await writeStream(saveLocation, res.body); log.info("clip model downloaded"); } @@ -110,8 +96,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") { const localFileSize = (await fs.stat(modelSavePath)).size; if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) { log.info( - "clip image model size mismatch, downloading again got:", - localFileSize, + `clip image model size mismatch, downloading again got: ${localFileSize}`, ); imageModelDownloadInProgress = downloadModel( modelSavePath, @@ -149,8 +134,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") { const localFileSize = (await fs.stat(modelSavePath)).size; if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) { log.info( - "clip text model size mismatch, downloading again got:", - localFileSize, + `clip text model size mismatch, downloading again got: ${localFileSize}`, ); textModelDownloadInProgress = true; downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type]) @@ -210,7 +194,51 @@ function getTokenizer() { return tokenizer; } -export async function computeImageEmbedding( +export const computeImageEmbedding = async ( + model: Model, + imageData: Uint8Array, +): Promise => { + let tempInputFilePath = null; + try { + tempInputFilePath = await generateTempFilePath(""); + const imageStream = new Response(imageData.buffer).body; + await writeStream(tempInputFilePath, imageStream); + const embedding = await computeImageEmbedding_( + model, + tempInputFilePath, + ); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } finally { + if (tempInputFilePath) { + await deleteTempFile(tempInputFilePath); + } + } +}; + +const isExecError = (err: any) => { + return err.message.includes("Command failed:"); +}; + +const parseExecError = (err: any) => { + const errMessage = err.message; + if (errMessage.includes("Bad CPU type in executable")) { + return CustomErrors.UNSUPPORTED_PLATFORM( + process.platform, + process.arch, + ); + } else { + return errMessage; + } +}; + +async function computeImageEmbedding_( model: Model, inputFilePath: string, ): Promise { @@ -244,11 +272,7 @@ export async function computeGGMLImageEmbedding( } }); - const escapedCmd = shellescape(cmd); - log.info("running clip command", escapedCmd); - const startTime = Date.now(); - const { stdout } = await execAsync(escapedCmd); - log.info("clip command execution time ", Date.now() - startTime); + const { stdout } = await execAsync(cmd); // parse stdout and return embedding // get the last line of stdout const lines = stdout.split("\n"); @@ -257,7 +281,7 @@ export async function computeGGMLImageEmbedding( const embeddingArray = new Float32Array(embedding); return embeddingArray; } catch (err) { - logErrorSentry(err, "Error in computeGGMLImageEmbedding"); + log.error("Failed to compute GGML image embedding", err); throw err; } } @@ -282,7 +306,7 @@ export async function computeONNXImageEmbedding( const imageEmbedding = results["output"].data; // Float32Array return normalizeEmbedding(imageEmbedding); } catch (err) { - logErrorSentry(err, "Error in computeONNXImageEmbedding"); + log.error("Failed to compute ONNX image embedding", err); throw err; } } @@ -290,6 +314,23 @@ export async function computeONNXImageEmbedding( export async function computeTextEmbedding( model: Model, text: string, +): Promise { + try { + const embedding = computeTextEmbedding_(model, text); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } +} + +async function computeTextEmbedding_( + model: Model, + text: string, ): Promise { if (model === Model.GGML_CLIP) { return await computeGGMLTextEmbedding(text); @@ -316,11 +357,7 @@ export async function computeGGMLTextEmbedding( } }); - const escapedCmd = shellescape(cmd); - log.info("running clip command", escapedCmd); - const startTime = Date.now(); - const { stdout } = await execAsync(escapedCmd); - log.info("clip command execution time ", Date.now() - startTime); + const { stdout } = await execAsync(cmd); // parse stdout and return embedding // get the last line of stdout const lines = stdout.split("\n"); @@ -332,7 +369,7 @@ export async function computeGGMLTextEmbedding( if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) { log.info(CustomErrors.MODEL_DOWNLOAD_PENDING); } else { - logErrorSentry(err, "Error in computeGGMLTextEmbedding"); + log.error("Failed to compute GGML text embedding", err); } throw err; } @@ -369,7 +406,7 @@ export async function computeONNXTextEmbedding( } async function getRGBData(inputFilePath: string) { - const jpegData = await readFile(inputFilePath); + const jpegData = await fs.readFile(inputFilePath); let rawImageData; try { rawImageData = jpeg.decode(jpegData, { diff --git a/desktop/src/services/diskCache.ts b/desktop/src/services/diskCache.ts deleted file mode 100644 index 6861bba9f..000000000 --- a/desktop/src/services/diskCache.ts +++ /dev/null @@ -1,98 +0,0 @@ -import crypto from "crypto"; -import path from "path"; -import { existsSync, rename, stat, unlink } from "promise-fs"; -import DiskLRUService from "../services/diskLRU"; -import { LimitedCache } from "../types/cache"; -import { getFileStream, writeStream } from "./fs"; -import { logError } from "./logging"; - -const DEFAULT_CACHE_LIMIT = 1000 * 1000 * 1000; // 1GB - -export class DiskCache implements LimitedCache { - constructor( - private cacheBucketDir: string, - private cacheLimit = DEFAULT_CACHE_LIMIT, - ) {} - - async put(cacheKey: string, response: Response): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - await writeStream(cachePath, response.body); - DiskLRUService.enforceCacheSizeLimit( - this.cacheBucketDir, - this.cacheLimit, - ); - } - - async match( - cacheKey: string, - { sizeInBytes }: { sizeInBytes?: number } = {}, - ): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - const fileStats = await stat(cachePath); - if (sizeInBytes && fileStats.size !== sizeInBytes) { - logError( - Error(), - "Cache key exists but size does not match. Deleting cache key.", - ); - unlink(cachePath).catch((e) => { - if (e.code === "ENOENT") return; - logError(e, "Failed to delete cache key"); - }); - return undefined; - } - DiskLRUService.touch(cachePath); - return new Response(await getFileStream(cachePath)); - } else { - // add fallback for old cache keys - const oldCachePath = getOldAssetCachePath( - this.cacheBucketDir, - cacheKey, - ); - if (existsSync(oldCachePath)) { - const fileStats = await stat(oldCachePath); - if (sizeInBytes && fileStats.size !== sizeInBytes) { - logError( - Error(), - "Old cache key exists but size does not match. Deleting cache key.", - ); - unlink(oldCachePath).catch((e) => { - if (e.code === "ENOENT") return; - logError(e, "Failed to delete cache key"); - }); - return undefined; - } - const match = new Response(await getFileStream(oldCachePath)); - void migrateOldCacheKey(oldCachePath, cachePath); - return match; - } - return undefined; - } - } - async delete(cacheKey: string): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - await unlink(cachePath); - return true; - } else { - return false; - } - } -} - -function getOldAssetCachePath(cacheDir: string, cacheKey: string) { - // hashing the key to prevent illegal filenames - const cacheKeyHash = crypto - .createHash("sha256") - .update(cacheKey) - .digest("hex"); - return path.join(cacheDir, cacheKeyHash); -} - -async function migrateOldCacheKey(oldCacheKey: string, newCacheKey: string) { - try { - await rename(oldCacheKey, newCacheKey); - } catch (e) { - logError(e, "Failed to move cache key to new cache key"); - } -} diff --git a/desktop/src/services/diskLRU.ts b/desktop/src/services/diskLRU.ts deleted file mode 100644 index 44b05c099..000000000 --- a/desktop/src/services/diskLRU.ts +++ /dev/null @@ -1,105 +0,0 @@ -import getFolderSize from "get-folder-size"; -import path from "path"; -import { close, open, readdir, stat, unlink, utimes } from "promise-fs"; -import { logError } from "../services/logging"; - -export interface LeastRecentlyUsedResult { - atime: Date; - path: string; -} - -class DiskLRUService { - private isRunning: Promise = null; - private reRun: boolean = false; - - async touch(path: string) { - try { - const time = new Date(); - await utimes(path, time, time); - } catch (err) { - logError(err, "utimes method touch failed"); - try { - await close(await open(path, "w")); - } catch (e) { - logError(e, "open-close method touch failed"); - } - // log and ignore - } - } - - enforceCacheSizeLimit(cacheDir: string, maxSize: number) { - if (!this.isRunning) { - this.isRunning = this.evictLeastRecentlyUsed(cacheDir, maxSize); - this.isRunning.then(() => { - this.isRunning = null; - if (this.reRun) { - this.reRun = false; - this.enforceCacheSizeLimit(cacheDir, maxSize); - } - }); - } else { - this.reRun = true; - } - } - - async evictLeastRecentlyUsed(cacheDir: string, maxSize: number) { - try { - await new Promise((resolve) => { - getFolderSize(cacheDir, async (err, size) => { - if (err) { - throw err; - } - if (size >= maxSize) { - const leastRecentlyUsed = - await this.findLeastRecentlyUsed(cacheDir); - try { - await unlink(leastRecentlyUsed.path); - } catch (e) { - // ENOENT: File not found - // which can be ignored as we are trying to delete the file anyway - if (e.code !== "ENOENT") { - logError( - e, - "Failed to evict least recently used", - ); - } - // ignoring the error, as it would get retried on the next run - } - this.evictLeastRecentlyUsed(cacheDir, maxSize); - } - resolve(null); - }); - }); - } catch (e) { - logError(e, "evictLeastRecentlyUsed failed"); - } - } - - private async findLeastRecentlyUsed( - dir: string, - result?: LeastRecentlyUsedResult, - ): Promise { - result = result || { atime: new Date(), path: "" }; - - const files = await readdir(dir); - for (const file of files) { - const newBase = path.join(dir, file); - const stats = await stat(newBase); - if (stats.isDirectory()) { - result = await this.findLeastRecentlyUsed(newBase, result); - } else { - const { atime } = await stat(newBase); - - if (atime.getTime() < result.atime.getTime()) { - result = { - atime, - path: newBase, - }; - } - } - } - return result; - } -} - -export default new DiskLRUService(); diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index e0a915790..ddb3361cf 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/desktop/src/services/ffmpeg.ts @@ -1,26 +1,76 @@ -import log from "electron-log"; import pathToFfmpeg from "ffmpeg-static"; -import { existsSync } from "fs"; -import { readFile, rmSync, writeFile } from "promise-fs"; -import util from "util"; -import { promiseWithTimeout } from "../utils/common"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import { CustomErrors } from "../constants/errors"; +import { writeStream } from "../main/fs"; +import log from "../main/log"; +import { execAsync } from "../main/util"; +import { ElectronFile } from "../types/ipc"; import { generateTempFilePath, getTempDirPath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); - -const execAsync = util.promisify(require("child_process").exec); - -const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; const INPUT_PATH_PLACEHOLDER = "INPUT"; const FFMPEG_PLACEHOLDER = "FFMPEG"; const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; -function getFFmpegStaticPath() { - return pathToFfmpeg.replace("app.asar", "app.asar.unpacked"); +/** + * 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 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 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 + * + * I'm not sure if our code is supposed to be able to use it, and how. + */ +export async function runFFmpegCmd( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, +) { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (!existsSync(inputFile.path)) { + const tempFilePath = await generateTempFilePath(inputFile.name); + await writeStream(tempFilePath, await inputFile.stream()); + inputFilePath = tempFilePath; + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const outputFileData = await runFFmpegCmd_( + cmd, + inputFilePath, + outputFileName, + dontTimeout, + ); + return new File([outputFileData], outputFileName); + } finally { + if (createdTempInputFile) { + await deleteTempFile(inputFilePath); + } + } } -export async function runFFmpegCmd( +export async function runFFmpegCmd_( cmd: string[], inputFilePath: string, outputFileName: string, @@ -32,7 +82,7 @@ export async function runFFmpegCmd( cmd = cmd.map((cmdPart) => { if (cmdPart === FFMPEG_PLACEHOLDER) { - return getFFmpegStaticPath(); + return ffmpegBinaryPath(); } else if (cmdPart === INPUT_PATH_PLACEHOLDER) { return inputFilePath; } else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { @@ -41,54 +91,72 @@ export async function runFFmpegCmd( return cmdPart; } }); - const escapedCmd = shellescape(cmd); - log.info("running ffmpeg command", escapedCmd); - const startTime = Date.now(); + if (dontTimeout) { - await execAsync(escapedCmd); + await execAsync(cmd); } else { - await promiseWithTimeout( - execAsync(escapedCmd), - FFMPEG_EXECUTION_WAIT_TIME, - ); + await promiseWithTimeout(execAsync(cmd), 30 * 1000); } + if (!existsSync(tempOutputFilePath)) { throw new Error("ffmpeg output file not found"); } - log.info( - "ffmpeg command execution time ", - escapedCmd, - Date.now() - startTime, - "ms", - ); - - const outputFile = await readFile(tempOutputFilePath); + const outputFile = await fs.readFile(tempOutputFilePath); return new Uint8Array(outputFile); } catch (e) { - logErrorSentry(e, "ffmpeg run command error"); + log.error("FFMPEG command failed", e); throw e; } finally { - try { - rmSync(tempOutputFilePath, { force: true }); - } catch (e) { - logErrorSentry(e, "failed to remove tempOutputFile"); - } + await deleteTempFile(tempOutputFilePath); } } +/** + * 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"); +}; + export async function writeTempFile(fileStream: Uint8Array, fileName: string) { const tempFilePath = await generateTempFilePath(fileName); - await writeFile(tempFilePath, fileStream); + await fs.writeFile(tempFilePath, fileStream); return tempFilePath; } export async function deleteTempFile(tempFilePath: string) { const tempDirPath = await getTempDirPath(); - if (!tempFilePath.startsWith(tempDirPath)) { - logErrorSentry( - Error("not a temp file"), - "tried to delete a non temp file", - ); - } - rmSync(tempFilePath, { force: true }); + if (!tempFilePath.startsWith(tempDirPath)) + log.error("Attempting to delete a non-temp file ${tempFilePath}"); + await fs.rm(tempFilePath, { force: true }); } + +const promiseWithTimeout = async ( + request: Promise, + timeout: number, +): Promise => { + const timeoutRef: { + current: NodeJS.Timeout; + } = { current: null }; + const rejectOnTimeout = new Promise((_, reject) => { + timeoutRef.current = setTimeout( + () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), + timeout, + ); + }); + const requestWithTimeOutCancellation = async () => { + const resp = await request; + clearTimeout(timeoutRef.current); + return resp; + }; + return await Promise.race([ + requestWithTimeOutCancellation(), + rejectOnTimeout, + ]); +}; diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index 06d413c1f..d36317720 100644 --- a/desktop/src/services/fs.ts +++ b/desktop/src/services/fs.ts @@ -1,13 +1,18 @@ -import { existsSync } from "fs"; import StreamZip from "node-stream-zip"; -import path from "path"; -import * as fs from "promise-fs"; -import { Readable } from "stream"; -import { ElectronFile } from "../types"; -import { logError } from "./logging"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { logError } from "../main/log"; +import { ElectronFile } from "../types/ipc"; const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; +export async function getDirFiles(dirPath: string) { + const files = await getDirFilePaths(dirPath); + const electronFiles = await Promise.all(files.map(getElectronFile)); + return electronFiles; +} + // https://stackoverflow.com/a/63111390 export const getDirFilePaths = async (dirPath: string) => { if (!(await fs.stat(dirPath)).isDirectory()) { @@ -25,16 +30,14 @@ export const getDirFilePaths = async (dirPath: string) => { return files; }; -export const getFileStream = async (filePath: string) => { +const getFileStream = async (filePath: string) => { const file = await fs.open(filePath, "r"); let offset = 0; const readableStream = new ReadableStream({ async pull(controller) { try { const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE); - // original types were not working correctly - const bytesRead = (await fs.read( - file, + const bytesRead = (await file.read( buff, 0, FILE_STREAM_CHUNK_SIZE, @@ -43,16 +46,16 @@ export const getFileStream = async (filePath: string) => { offset += bytesRead; if (bytesRead === 0) { controller.close(); - await fs.close(file); + await file.close(); } else { controller.enqueue(buff.slice(0, bytesRead)); } } catch (e) { - await fs.close(file); + await file.close(); } }, async cancel() { - await fs.close(file); + await file.close(); }, }); return readableStream; @@ -183,134 +186,3 @@ export const getZipFileStream = async ( }); return readableStream; }; - -export async function isFolder(dirPath: string) { - try { - const stats = await fs.stat(dirPath); - return stats.isDirectory(); - } catch (e) { - let err = e; - // if code is defined, it's an error from fs.stat - if (typeof e.code !== "undefined") { - // ENOENT means the file does not exist - if (e.code === "ENOENT") { - return false; - } - err = Error(`fs error code: ${e.code}`); - } - logError(err, "isFolder failed"); - return false; - } -} - -export const convertBrowserStreamToNode = ( - fileStream: ReadableStream, -) => { - const reader = fileStream.getReader(); - const rs = new Readable(); - - rs._read = async () => { - try { - const result = await reader.read(); - - if (!result.done) { - rs.push(Buffer.from(result.value)); - } else { - rs.push(null); - return; - } - } catch (e) { - rs.emit("error", e); - } - }; - - return rs; -}; - -export async function writeNodeStream( - filePath: string, - fileStream: NodeJS.ReadableStream, -) { - const writeable = fs.createWriteStream(filePath); - - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error - }); - - fileStream.pipe(writeable); - - await new Promise((resolve, reject) => { - writeable.on("finish", resolve); - writeable.on("error", async (e) => { - if (existsSync(filePath)) { - await fs.unlink(filePath); - } - reject(e); - }); - }); -} - -export async function writeStream( - filePath: string, - fileStream: ReadableStream, -) { - const readable = convertBrowserStreamToNode(fileStream); - await writeNodeStream(filePath, readable); -} - -export async function readTextFile(filePath: string) { - if (!existsSync(filePath)) { - throw new Error("File does not exist"); - } - return await fs.readFile(filePath, "utf-8"); -} - -export async function moveFile( - sourcePath: string, - destinationPath: string, -): Promise { - if (!existsSync(sourcePath)) { - throw new Error("File does not exist"); - } - if (existsSync(destinationPath)) { - throw new Error("Destination file already exists"); - } - // check if destination folder exists - const destinationFolder = path.dirname(destinationPath); - if (!existsSync(destinationFolder)) { - await fs.mkdir(destinationFolder, { recursive: true }); - } - await fs.rename(sourcePath, destinationPath); -} - -export async function deleteFolder(folderPath: string): Promise { - if (!existsSync(folderPath)) { - return; - } - if (!fs.statSync(folderPath).isDirectory()) { - throw new Error("Path is not a folder"); - } - // check if folder is empty - const files = await fs.readdir(folderPath); - if (files.length > 0) { - throw new Error("Folder is not empty"); - } - await fs.rmdir(folderPath); -} - -export async function rename(oldPath: string, newPath: string) { - if (!existsSync(oldPath)) { - throw new Error("Path does not exist"); - } - await fs.rename(oldPath, newPath); -} - -export function deleteFile(filePath: string): void { - if (!existsSync(filePath)) { - return; - } - if (!fs.statSync(filePath).isFile()) { - throw new Error("Path is not a file"); - } - fs.rmSync(filePath); -} diff --git a/desktop/src/services/imageProcessor.ts b/desktop/src/services/imageProcessor.ts index cb6c7416d..f6a567f8c 100644 --- a/desktop/src/services/imageProcessor.ts +++ b/desktop/src/services/imageProcessor.ts @@ -1,18 +1,14 @@ -import { exec } from "child_process"; -import util from "util"; - -import log from "electron-log"; -import { existsSync, rmSync } from "fs"; +import { existsSync } from "fs"; +import fs from "node:fs/promises"; import path from "path"; -import { readFile, writeFile } from "promise-fs"; import { CustomErrors } from "../constants/errors"; -import { isDev } from "../utils/common"; +import { writeStream } from "../main/fs"; +import { logError, logErrorSentry } from "../main/log"; +import { execAsync, isDev } from "../main/util"; +import { ElectronFile } from "../types/ipc"; import { isPlatform } from "../utils/common/platform"; import { generateTempFilePath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); - -const asyncExec = util.promisify(exec); +import { deleteTempFile } from "./ffmpeg"; const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK"; const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION"; @@ -59,10 +55,10 @@ const IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE = [ const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [ IMAGE_MAGICK_PLACEHOLDER, + INPUT_PATH_PLACEHOLDER, "-auto-orient", "-define", `jpeg:size=${SAMPLE_SIZE_PLACEHOLDER}x${SAMPLE_SIZE_PLACEHOLDER}`, - INPUT_PATH_PLACEHOLDER, "-thumbnail", `${MAX_DIMENSION_PLACEHOLDER}x${MAX_DIMENSION_PLACEHOLDER}>`, "-unsharp", @@ -81,6 +77,17 @@ function getImageMagickStaticPath() { export async function convertToJPEG( fileData: Uint8Array, filename: string, +): Promise { + if (isPlatform("windows")) { + throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED); + } + const convertedFileData = await convertToJPEG_(fileData, filename); + return convertedFileData; +} + +async function convertToJPEG_( + fileData: Uint8Array, + filename: string, ): Promise { let tempInputFilePath: string; let tempOutputFilePath: string; @@ -88,47 +95,30 @@ export async function convertToJPEG( tempInputFilePath = await generateTempFilePath(filename); tempOutputFilePath = await generateTempFilePath("output.jpeg"); - await writeFile(tempInputFilePath, fileData); + await fs.writeFile(tempInputFilePath, fileData); - await runConvertCommand(tempInputFilePath, tempOutputFilePath); - - if (!existsSync(tempOutputFilePath)) { - throw new Error("heic convert output file not found"); - } - const convertedFileData = new Uint8Array( - await readFile(tempOutputFilePath), + await execAsync( + constructConvertCommand(tempInputFilePath, tempOutputFilePath), ); - return convertedFileData; + + return new Uint8Array(await fs.readFile(tempOutputFilePath)); } catch (e) { logErrorSentry(e, "failed to convert heic"); throw e; } finally { try { - rmSync(tempInputFilePath, { force: true }); + await fs.rm(tempInputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempInputFile"); } try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } } } -async function runConvertCommand( - tempInputFilePath: string, - tempOutputFilePath: string, -) { - const convertCmd = constructConvertCommand( - tempInputFilePath, - tempOutputFilePath, - ); - const escapedCmd = shellescape(convertCmd); - log.info("running convert command: " + escapedCmd); - await asyncExec(escapedCmd); -} - function constructConvertCommand( tempInputFilePath: string, tempOutputFilePath: string, @@ -166,6 +156,44 @@ function constructConvertCommand( } export async function generateImageThumbnail( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number, +): Promise { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (isPlatform("windows")) { + throw Error( + CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED, + ); + } + if (!existsSync(inputFile.path)) { + const tempFilePath = await generateTempFilePath(inputFile.name); + await writeStream(tempFilePath, await inputFile.stream()); + inputFilePath = tempFilePath; + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const thumbnail = await generateImageThumbnail_( + inputFilePath, + maxDimension, + maxSize, + ); + return thumbnail; + } finally { + if (createdTempInputFile) { + try { + await deleteTempFile(inputFilePath); + } catch (e) { + logError(e, "failed to deleteTempFile"); + } + } + } +} + +async function generateImageThumbnail_( inputFilePath: string, width: number, maxSize: number, @@ -176,17 +204,15 @@ export async function generateImageThumbnail( tempOutputFilePath = await generateTempFilePath("thumb.jpeg"); let thumbnail: Uint8Array; do { - await runThumbnailGenerationCommand( - inputFilePath, - tempOutputFilePath, - width, - quality, + await execAsync( + constructThumbnailGenerationCommand( + inputFilePath, + tempOutputFilePath, + width, + quality, + ), ); - - if (!existsSync(tempOutputFilePath)) { - throw new Error("output thumbnail file not found"); - } - thumbnail = new Uint8Array(await readFile(tempOutputFilePath)); + thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath)); quality -= 10; } while (thumbnail.length > maxSize && quality > MIN_QUALITY); return thumbnail; @@ -195,30 +221,13 @@ export async function generateImageThumbnail( throw e; } finally { try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } } } -async function runThumbnailGenerationCommand( - inputFilePath: string, - tempOutputFilePath: string, - maxDimension: number, - quality: number, -) { - const thumbnailGenerationCmd: string[] = - constructThumbnailGenerationCommand( - inputFilePath, - tempOutputFilePath, - maxDimension, - quality, - ); - const escapedCmd = shellescape(thumbnailGenerationCmd); - log.info("running thumbnail generation command: " + escapedCmd); - await asyncExec(escapedCmd); -} function constructThumbnailGenerationCommand( inputFilePath: string, tempOutputFilePath: string, diff --git a/desktop/src/services/logging.ts b/desktop/src/services/logging.ts deleted file mode 100644 index bcbacd9f5..000000000 --- a/desktop/src/services/logging.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ipcRenderer } from "electron"; -import log from "electron-log"; - -export function logToDisk(logLine: string) { - log.info(logLine); -} - -export function openLogDirectory() { - ipcRenderer.invoke("open-log-dir"); -} - -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke("log-error", error, message, info); -} diff --git a/desktop/src/services/sentry.ts b/desktop/src/services/sentry.ts deleted file mode 100644 index 4c5573152..000000000 --- a/desktop/src/services/sentry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isDev } from "../utils/common"; -import { logToDisk } from "./logging"; - -/** Deprecated, but no alternative yet */ -export function logErrorSentry( - error: any, - msg: string, - info?: Record, -) { - logToDisk( - `error: ${error?.name} ${error?.message} ${ - error?.stack - } msg: ${msg} info: ${JSON.stringify(info)}`, - ); - if (isDev) { - console.log(error, { msg, info }); - } -} diff --git a/desktop/src/services/upload.ts b/desktop/src/services/upload.ts index 38a628c25..2fc56fef5 100644 --- a/desktop/src/services/upload.ts +++ b/desktop/src/services/upload.ts @@ -1,7 +1,8 @@ import StreamZip from "node-stream-zip"; import path from "path"; import { uploadStatusStore } from "../stores/upload.store"; -import { ElectronFile, FILE_PATH_KEYS, FILE_PATH_TYPE } from "../types"; +import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc"; +import { FILE_PATH_KEYS } from "../types/main"; import { getValidPaths, getZipFileStream } from "./fs"; export const getSavedFilePaths = (type: FILE_PATH_TYPE) => { diff --git a/desktop/src/services/userPreference.ts b/desktop/src/services/userPreference.ts index e3c6db290..8074ee4de 100644 --- a/desktop/src/services/userPreference.ts +++ b/desktop/src/services/userPreference.ts @@ -31,11 +31,3 @@ export function clearSkipAppVersion() { export function clearMuteUpdateNotificationVersion() { userPreferencesStore.delete("muteUpdateNotificationVersion"); } - -export function setCustomCacheDirectory(directory: string) { - userPreferencesStore.set("customCacheDirectory", directory); -} - -export function getCustomCacheDirectory(): string { - return userPreferencesStore.get("customCacheDirectory"); -} diff --git a/desktop/src/services/watch.ts b/desktop/src/services/watch.ts index 8b7746964..3505be744 100644 --- a/desktop/src/services/watch.ts +++ b/desktop/src/services/watch.ts @@ -1,11 +1,95 @@ +import type { FSWatcher } from "chokidar"; +import ElectronLog from "electron-log"; import { watchStore } from "../stores/watch.store"; -import { WatchStoreType } from "../types"; +import { WatchMapping, WatchStoreType } from "../types/ipc"; +import { isMappingPresent } from "../utils/watch"; + +export const addWatchMapping = async ( + watcher: FSWatcher, + rootFolderName: string, + folderPath: string, + uploadStrategy: number, +) => { + ElectronLog.log(`Adding watch mapping: ${folderPath}`); + const watchMappings = getWatchMappings(); + if (isMappingPresent(watchMappings, folderPath)) { + throw new Error(`Watch mapping already exists`); + } + + watcher.add(folderPath); + + watchMappings.push({ + rootFolderName, + uploadStrategy, + folderPath, + syncedFiles: [], + ignoredFiles: [], + }); + + setWatchMappings(watchMappings); +}; + +export const removeWatchMapping = async ( + watcher: FSWatcher, + folderPath: string, +) => { + let watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw new Error(`Watch mapping does not exist`); + } + + watcher.unwatch(watchMapping.folderPath); + + watchMappings = watchMappings.filter( + (mapping) => mapping.folderPath !== watchMapping.folderPath, + ); + + setWatchMappings(watchMappings); +}; + +export function updateWatchMappingSyncedFiles( + folderPath: string, + files: WatchMapping["syncedFiles"], +): void { + const watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw Error(`Watch mapping not found`); + } + + watchMapping.syncedFiles = files; + setWatchMappings(watchMappings); +} + +export function updateWatchMappingIgnoredFiles( + folderPath: string, + files: WatchMapping["ignoredFiles"], +): void { + const watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw Error(`Watch mapping not found`); + } + + watchMapping.ignoredFiles = files; + setWatchMappings(watchMappings); +} export function getWatchMappings() { const mappings = watchStore.get("mappings") ?? []; return mappings; } -export function setWatchMappings(watchMappings: WatchStoreType["mappings"]) { +function setWatchMappings(watchMappings: WatchStoreType["mappings"]) { watchStore.set("mappings", watchMappings); } diff --git a/desktop/src/stores/keys.store.ts b/desktop/src/stores/keys.store.ts index 943bdb1ca..d112f045a 100644 --- a/desktop/src/stores/keys.store.ts +++ b/desktop/src/stores/keys.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { KeysStoreType } from "../types"; +import type { KeysStoreType } from "../types/main"; const keysStoreSchema: Schema = { AnonymizeUserID: { diff --git a/desktop/src/stores/safeStorage.store.ts b/desktop/src/stores/safeStorage.store.ts index 7822e32e3..809c9623f 100644 --- a/desktop/src/stores/safeStorage.store.ts +++ b/desktop/src/stores/safeStorage.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { SafeStorageStoreType } from "../types"; +import type { SafeStorageStoreType } from "../types/main"; const safeStorageSchema: Schema = { encryptionKey: { diff --git a/desktop/src/stores/upload.store.ts b/desktop/src/stores/upload.store.ts index b918fd283..5ede1fb99 100644 --- a/desktop/src/stores/upload.store.ts +++ b/desktop/src/stores/upload.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { UploadStoreType } from "../types"; +import type { UploadStoreType } from "../types/main"; const uploadStoreSchema: Schema = { filePaths: { diff --git a/desktop/src/stores/userPreferences.store.ts b/desktop/src/stores/userPreferences.store.ts index e6fec425a..9545b1261 100644 --- a/desktop/src/stores/userPreferences.store.ts +++ b/desktop/src/stores/userPreferences.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { UserPreferencesType } from "../types"; +import type { UserPreferencesType } from "../types/main"; const userPreferencesSchema: Schema = { hideDockIcon: { @@ -11,9 +11,6 @@ const userPreferencesSchema: Schema = { muteUpdateNotificationVersion: { type: "string", }, - customCacheDirectory: { - type: "string", - }, }; export const userPreferencesStore = new Store({ diff --git a/desktop/src/stores/watch.store.ts b/desktop/src/stores/watch.store.ts index 6489ba3e8..cbc71dde7 100644 --- a/desktop/src/stores/watch.store.ts +++ b/desktop/src/stores/watch.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { WatchStoreType } from "../types"; +import { WatchStoreType } from "../types/ipc"; const watchStoreSchema: Schema = { mappings: { diff --git a/desktop/src/types/any-shell-escape.d.ts b/desktop/src/types/any-shell-escape.d.ts new file mode 100644 index 000000000..4172cdb1e --- /dev/null +++ b/desktop/src/types/any-shell-escape.d.ts @@ -0,0 +1,25 @@ +/** + * Escape and stringify an array of arguments to be executed on the shell. + * + * @example + * + * const shellescape = require('any-shell-escape'); + * + * const args = ['curl', '-v', '-H', 'Location;', '-H', "User-Agent: FooBar's so-called \"Browser\"", 'http://www.daveeddy.com/?name=dave&age=24']; + * + * const escaped = shellescape(args); + * console.log(escaped); + * + * yields (on POSIX shells): + * + * curl -v -H 'Location;' -H 'User-Agent: FoorBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24' + * + * or (on Windows): + * + * curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24" +Which is suitable for being executed by the shell. + */ +declare module "any-shell-escape" { + declare const shellescape: (args: readonly string | string[]) => string; + export default shellescape; +} diff --git a/desktop/src/types/autoLauncher.ts b/desktop/src/types/autoLauncher.ts deleted file mode 100644 index 9f82d2014..000000000 --- a/desktop/src/types/autoLauncher.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface AutoLauncherClient { - isEnabled: () => Promise; - toggleAutoLaunch: () => Promise; - wasAutoLaunched: () => Promise; -} diff --git a/desktop/src/types/cache.ts b/desktop/src/types/cache.ts deleted file mode 100644 index 112716eea..000000000 --- a/desktop/src/types/cache.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface LimitedCache { - match: ( - key: string, - options?: { sizeInBytes?: number }, - ) => Promise; - put: (key: string, data: Response) => Promise; - delete: (key: string) => Promise; -} diff --git a/desktop/src/types/index.ts b/desktop/src/types/ipc.ts similarity index 54% rename from desktop/src/types/index.ts rename to desktop/src/types/ipc.ts index 208983826..93586f29a 100644 --- a/desktop/src/types/index.ts +++ b/desktop/src/types/ipc.ts @@ -1,3 +1,21 @@ +/** + * @file types that are shared across the IPC boundary with the renderer process + * + * This file is manually kept in sync with the renderer code. + * See [Note: types.ts <-> preload.ts <-> ipc.ts] + */ +/** + * Deprecated - Use File + webUtils.getPathForFile instead + * + * Electron used to augment the standard web + * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object with an + * additional `path` property. This is now deprecated, and will be removed in a + * future release. + * https://www.electronjs.org/docs/latest/api/file-object + * + * The alternative to the `path` property is to use `webUtils.getPathForFile` + * https://www.electronjs.org/docs/latest/api/web-utils + */ export interface ElectronFile { name: string; path: string; @@ -8,18 +26,6 @@ export interface ElectronFile { arrayBuffer: () => Promise; } -export interface UploadStoreType { - filePaths: string[]; - zipPaths: string[]; - collectionName: string; -} - -export interface KeysStoreType { - AnonymizeUserID: { - id: string; - }; -} - interface WatchMappingSyncedFile { path: string; uploadedFileID: number; @@ -43,33 +49,11 @@ export enum FILE_PATH_TYPE { ZIPS = "zips", } -export const FILE_PATH_KEYS: { - [k in FILE_PATH_TYPE]: keyof UploadStoreType; -} = { - [FILE_PATH_TYPE.ZIPS]: "zipPaths", - [FILE_PATH_TYPE.FILES]: "filePaths", -}; - -export interface SafeStorageStoreType { - encryptionKey: string; -} - -export interface UserPreferencesType { - hideDockIcon: boolean; - skipAppVersion: string; - muteUpdateNotificationVersion: string; - customCacheDirectory: string; -} - export interface AppUpdateInfo { autoUpdatable: boolean; version: string; } -export interface GetFeatureFlagResponse { - desktopCutoffVersion?: string; -} - export enum Model { GGML_CLIP = "ggml-clip", ONNX_CLIP = "onnx-clip", diff --git a/desktop/src/types/main.ts b/desktop/src/types/main.ts new file mode 100644 index 000000000..98d04ec6e --- /dev/null +++ b/desktop/src/types/main.ts @@ -0,0 +1,36 @@ +import { FILE_PATH_TYPE } from "./ipc"; + +export interface AutoLauncherClient { + isEnabled: () => Promise; + toggleAutoLaunch: () => Promise; + wasAutoLaunched: () => Promise; +} + +export interface UploadStoreType { + filePaths: string[]; + zipPaths: string[]; + collectionName: string; +} + +export interface KeysStoreType { + AnonymizeUserID: { + id: string; + }; +} + +export const FILE_PATH_KEYS: { + [k in FILE_PATH_TYPE]: keyof UploadStoreType; +} = { + [FILE_PATH_TYPE.ZIPS]: "zipPaths", + [FILE_PATH_TYPE.FILES]: "filePaths", +}; + +export interface SafeStorageStoreType { + encryptionKey: string; +} + +export interface UserPreferencesType { + hideDockIcon: boolean; + skipAppVersion: string; + muteUpdateNotificationVersion: string; +} diff --git a/desktop/src/utils/clip-bpe-ts/README.md b/desktop/src/utils/clip-bpe-ts/README.md index ee052eb41..48b4c9423 100644 --- a/desktop/src/utils/clip-bpe-ts/README.md +++ b/desktop/src/utils/clip-bpe-ts/README.md @@ -1,6 +1,7 @@ # CLIP Byte Pair Encoding JavaScript Port -A JavaScript port of [OpenAI's CLIP byte-pair-encoding tokenizer](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py). +A JavaScript port of +[OpenAI's CLIP byte-pair-encoding tokenizer](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py). ```js import Tokenizer from "https://deno.land/x/clip_bpe@v0.0.6/mod.js"; @@ -18,10 +19,22 @@ t.encode("hello world!"); // [3306, 1002, 256] t.encodeForCLIP("hello world!"); // [49406,3306,1002,256,49407,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ``` -This encoder/decoder behaves differently to the the GPT-2/3 tokenizer (JavaScript version of that [here](https://github.com/latitudegames/GPT-3-Encoder)). For example, it doesn't preserve capital letters, as shown above. +This encoder/decoder behaves differently to the the GPT-2/3 tokenizer +(JavaScript version of that +[here](https://github.com/latitudegames/GPT-3-Encoder)). For example, it doesn't +preserve capital letters, as shown above. -The [Python version](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py) of this tokenizer uses the `ftfy` module to clean up the text before encoding it. I didn't include that module by default because currently the only version available in JavaScript is [this one](https://github.com/josephrocca/ftfy-pyodide), which requires importing a full Python runtime as a WebAssembly module. If you want the `ftfy` cleaning, just import it and clean your text with it before passing it to the `.encode()` method. +The +[Python version](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py) +of this tokenizer uses the `ftfy` module to clean up the text before encoding +it. I didn't include that module by default because currently the only version +available in JavaScript is +[this one](https://github.com/josephrocca/ftfy-pyodide), which requires +importing a full Python runtime as a WebAssembly module. If you want the `ftfy` +cleaning, just import it and clean your text with it before passing it to the +`.encode()` method. # License -To the extent that there is any original work in this repo, it is MIT Licensed, just like [openai/CLIP](https://github.com/openai/CLIP). +To the extent that there is any original work in this repo, it is MIT Licensed, +just like [openai/CLIP](https://github.com/openai/CLIP). diff --git a/desktop/src/utils/common/index.ts b/desktop/src/utils/common/index.ts deleted file mode 100644 index e970dfec4..000000000 --- a/desktop/src/utils/common/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { app } from "electron"; -import { CustomErrors } from "../../constants/errors"; -export const isDev = !app.isPackaged; - -export const promiseWithTimeout = async ( - request: Promise, - timeout: number, -): Promise => { - const timeoutRef: { - current: NodeJS.Timeout; - } = { current: null }; - const rejectOnTimeout = new Promise((_, reject) => { - timeoutRef.current = setTimeout( - () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), - timeout, - ); - }); - const requestWithTimeOutCancellation = async () => { - const resp = await request; - clearTimeout(timeoutRef.current); - return resp; - }; - return await Promise.race([ - requestWithTimeOutCancellation(), - rejectOnTimeout, - ]); -}; diff --git a/desktop/src/utils/cors.ts b/desktop/src/utils/cors.ts deleted file mode 100644 index 25f76211a..000000000 --- a/desktop/src/utils/cors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BrowserWindow } from "electron"; - -function lowerCaseHeaders(responseHeaders: Record) { - const headers: Record = {}; - for (const key of Object.keys(responseHeaders)) { - headers[key.toLowerCase()] = responseHeaders[key]; - } - return headers; -} - -export function addAllowOriginHeader(mainWindow: BrowserWindow) { - mainWindow.webContents.session.webRequest.onHeadersReceived( - (details, callback) => { - details.responseHeaders = lowerCaseHeaders(details.responseHeaders); - details.responseHeaders["access-control-allow-origin"] = ["*"]; - callback({ - responseHeaders: details.responseHeaders, - }); - }, - ); -} diff --git a/desktop/src/utils/createWindow.ts b/desktop/src/utils/createWindow.ts deleted file mode 100644 index c7d44e6c9..000000000 --- a/desktop/src/utils/createWindow.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { app, BrowserWindow, nativeImage } from "electron"; -import ElectronLog from "electron-log"; -import * as path from "path"; -import { isAppQuitting, rendererURL } from "../main"; -import autoLauncher from "../services/autoLauncher"; -import { logErrorSentry } from "../services/sentry"; -import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; -import { isPlatform } from "./common/platform"; - -export async function createWindow(): Promise { - const appImgPath = isDev - ? "resources/window-icon.png" - : path.join(process.resourcesPath, "window-icon.png"); - const appIcon = nativeImage.createFromPath(appImgPath); - // Create the browser window. - const mainWindow = new BrowserWindow({ - webPreferences: { - sandbox: false, - preload: path.join(__dirname, "../preload.js"), - contextIsolation: false, - }, - icon: appIcon, - show: false, // don't show the main window on load, - }); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - ElectronLog.log("wasAutoLaunched", wasAutoLaunched); - - const splash = new BrowserWindow({ - transparent: true, - show: false, - }); - if (isPlatform("mac") && wasAutoLaunched) { - app.dock.hide(); - } - if (!wasAutoLaunched) { - splash.maximize(); - splash.show(); - } - - if (isDev) { - splash.loadFile(`../resources/splash.html`); - mainWindow.loadURL(rendererURL); - // Open the DevTools. - mainWindow.webContents.openDevTools(); - } else { - splash.loadURL( - `file://${path.join(process.resourcesPath, "splash.html")}`, - ); - mainWindow.loadURL(rendererURL); - } - mainWindow.webContents.on("did-fail-load", () => { - splash.close(); - isDev - ? mainWindow.loadFile(`../resources/error.html`) - : splash.loadURL( - `file://${path.join(process.resourcesPath, "error.html")}`, - ); - mainWindow.maximize(); - mainWindow.show(); - }); - mainWindow.once("ready-to-show", async () => { - try { - splash.destroy(); - if (!wasAutoLaunched) { - mainWindow.maximize(); - mainWindow.show(); - } - } catch (e) { - // ignore - } - }); - mainWindow.webContents.on("render-process-gone", (event, details) => { - mainWindow.webContents.reload(); - logErrorSentry( - Error("render-process-gone"), - "webContents event render-process-gone", - { details }, - ); - ElectronLog.log("webContents event render-process-gone", details); - }); - mainWindow.webContents.on("unresponsive", () => { - mainWindow.webContents.forcefullyCrashRenderer(); - ElectronLog.log("webContents event unresponsive"); - }); - - setTimeout(() => { - try { - splash.destroy(); - if (!wasAutoLaunched) { - mainWindow.maximize(); - mainWindow.show(); - } - } catch (e) { - // ignore - } - }, 2000); - mainWindow.on("close", function (event) { - if (!isAppQuitting()) { - event.preventDefault(); - mainWindow.hide(); - } - return false; - }); - mainWindow.on("hide", () => { - const shouldHideDockIcon = getHideDockIconPreference(); - if (isPlatform("mac") && shouldHideDockIcon) { - app.dock.hide(); - } - }); - mainWindow.on("show", () => { - if (isPlatform("mac")) { - app.dock.show(); - } - }); - return mainWindow; -} diff --git a/desktop/src/utils/error.ts b/desktop/src/utils/error.ts deleted file mode 100644 index 1922045a2..000000000 --- a/desktop/src/utils/error.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CustomErrors } from "../constants/errors"; - -export const isExecError = (err: any) => { - return err.message.includes("Command failed:"); -}; - -export const parseExecError = (err: any) => { - const errMessage = err.message; - if (errMessage.includes("Bad CPU type in executable")) { - return CustomErrors.UNSUPPORTED_PLATFORM( - process.platform, - process.arch, - ); - } else { - return errMessage; - } -}; diff --git a/desktop/src/utils/events.ts b/desktop/src/utils/events.ts deleted file mode 100644 index 4c7ffe7d8..000000000 --- a/desktop/src/utils/events.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BrowserWindow } from "electron"; - -export function setupAppEventEmitter(mainWindow: BrowserWindow) { - // fire event when mainWindow is in foreground - mainWindow.on("focus", () => { - mainWindow.webContents.send("app-in-foreground"); - }); -} diff --git a/desktop/src/utils/ipcComms.ts b/desktop/src/utils/ipcComms.ts deleted file mode 100644 index eec644aeb..000000000 --- a/desktop/src/utils/ipcComms.ts +++ /dev/null @@ -1,194 +0,0 @@ -import chokidar from "chokidar"; -import { - app, - BrowserWindow, - dialog, - ipcMain, - Notification, - safeStorage, - shell, - Tray, -} from "electron"; -import path from "path"; -import { - getAppVersion, - muteUpdateNotification, - skipAppUpdate, - updateAndRestart, -} from "../services/appUpdater"; -import { - computeImageEmbedding, - computeTextEmbedding, -} from "../services/clipService"; -import { deleteTempFile, runFFmpegCmd } from "../services/ffmpeg"; -import { getDirFilePaths } from "../services/fs"; -import { - convertToJPEG, - generateImageThumbnail, -} from "../services/imageProcessor"; -import { logErrorSentry } from "../services/sentry"; -import { - getCustomCacheDirectory, - setCustomCacheDirectory, -} from "../services/userPreference"; -import { getPlatform } from "./common/platform"; -import { createWindow } from "./createWindow"; -import { generateTempFilePath } from "./temp"; - -export default function setupIpcComs( - tray: Tray, - mainWindow: BrowserWindow, - watcher: chokidar.FSWatcher, -): void { - ipcMain.handle("select-dir", async () => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory"], - }); - if (result.filePaths && result.filePaths.length > 0) { - return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep); - } - }); - - ipcMain.on("send-notification", (_, args) => { - const notification = { - title: "ente", - body: args, - }; - new Notification(notification).show(); - }); - ipcMain.on("reload-window", async () => { - const secondWindow = await createWindow(); - mainWindow.destroy(); - mainWindow = secondWindow; - }); - - ipcMain.handle("show-upload-files-dialog", async () => { - const files = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - }); - return files.filePaths; - }); - - ipcMain.handle("show-upload-zip-dialog", async () => { - const files = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - filters: [{ name: "Zip File", extensions: ["zip"] }], - }); - return files.filePaths; - }); - - ipcMain.handle("show-upload-dirs-dialog", async () => { - const dir = await dialog.showOpenDialog({ - properties: ["openDirectory", "multiSelections"], - }); - - let files: string[] = []; - for (const dirPath of dir.filePaths) { - files = [...files, ...(await getDirFilePaths(dirPath))]; - } - - return files; - }); - - ipcMain.handle("add-watcher", async (_, args: { dir: string }) => { - watcher.add(args.dir); - }); - - ipcMain.handle("remove-watcher", async (_, args: { dir: string }) => { - watcher.unwatch(args.dir); - }); - - ipcMain.handle("log-error", (_, err, msg, info?) => { - logErrorSentry(err, msg, info); - }); - - ipcMain.handle("safeStorage-encrypt", (_, message) => { - return safeStorage.encryptString(message); - }); - - ipcMain.handle("safeStorage-decrypt", (_, message) => { - return safeStorage.decryptString(message); - }); - - ipcMain.handle("get-path", (_, message) => { - // By default, these paths are at the following locations: - // - // * macOS: `~/Library/Application Support/ente` - // * Linux: `~/.config/ente` - // * Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` - // * Windows: C:\Users\\AppData\Local\ - // - // https://www.electronjs.org/docs/latest/api/app - return app.getPath(message); - }); - - ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => { - return convertToJPEG(fileData, filename); - }); - - ipcMain.handle("open-log-dir", () => { - shell.openPath(app.getPath("logs")); - }); - - ipcMain.handle("open-dir", (_, dirPath) => { - shell.openPath(path.normalize(dirPath)); - }); - - ipcMain.on("update-and-restart", () => { - updateAndRestart(); - }); - ipcMain.on("skip-app-update", (_, version) => { - skipAppUpdate(version); - }); - - ipcMain.on("mute-update-notification", (_, version) => { - muteUpdateNotification(version); - }); - - ipcMain.handle("get-app-version", () => { - return getAppVersion(); - }); - - ipcMain.handle( - "run-ffmpeg-cmd", - (_, cmd, inputFilePath, outputFileName, dontTimeout) => { - return runFFmpegCmd( - cmd, - inputFilePath, - outputFileName, - dontTimeout, - ); - }, - ); - ipcMain.handle("get-temp-file-path", (_, formatSuffix) => { - return generateTempFilePath(formatSuffix); - }); - ipcMain.handle("remove-temp-file", (_, tempFilePath: string) => { - return deleteTempFile(tempFilePath); - }); - - ipcMain.handle( - "generate-image-thumbnail", - (_, fileData, maxDimension, maxSize) => { - return generateImageThumbnail(fileData, maxDimension, maxSize); - }, - ); - - ipcMain.handle("compute-image-embedding", (_, model, inputFilePath) => { - return computeImageEmbedding(model, inputFilePath); - }); - ipcMain.handle("compute-text-embedding", (_, model, text) => { - return computeTextEmbedding(model, text); - }); - ipcMain.handle("get-platform", () => { - return getPlatform(); - }); - - ipcMain.handle("set-custom-cache-directory", (_, directory: string) => { - setCustomCacheDirectory(directory); - }); - - ipcMain.handle("get-custom-cache-directory", async () => { - return getCustomCacheDirectory(); - }); -} diff --git a/desktop/src/utils/logging.ts b/desktop/src/utils/logging.ts deleted file mode 100644 index 351a1aef8..000000000 --- a/desktop/src/utils/logging.ts +++ /dev/null @@ -1,24 +0,0 @@ -import log from "electron-log"; - -export function setupLogging(isDev?: boolean) { - log.transports.file.fileName = "ente.log"; - log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; - if (!isDev) { - log.transports.console.level = false; - } - log.transports.file.format = - "[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}"; -} - -export function convertBytesToHumanReadable( - bytes: number, - precision = 2, -): string { - if (bytes === 0 || isNaN(bytes)) { - return "0 MB"; - } - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; -} diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts deleted file mode 100644 index 569752326..000000000 --- a/desktop/src/utils/main.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron"; -import ElectronLog from "electron-log"; -import os from "os"; -import path from "path"; -import { existsSync } from "promise-fs"; -import util from "util"; -import { rendererURL } from "../main"; -import { setupAutoUpdater } from "../services/appUpdater"; -import autoLauncher from "../services/autoLauncher"; -import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; -import { isPlatform } from "./common/platform"; -import { buildContextMenu, buildMenuBar } from "./menu"; -const execAsync = util.promisify(require("child_process").exec); - -export async function handleUpdates(mainWindow: BrowserWindow) { - const isInstalledViaBrew = await checkIfInstalledViaBrew(); - if (!isDev && !isInstalledViaBrew) { - setupAutoUpdater(mainWindow); - } -} -export function setupTrayItem(mainWindow: BrowserWindow) { - const iconName = isPlatform("mac") - ? "taskbar-icon-Template.png" - : "taskbar-icon.png"; - const trayImgPath = path.join( - isDev ? "build" : process.resourcesPath, - iconName, - ); - const trayIcon = nativeImage.createFromPath(trayImgPath); - const tray = new Tray(trayIcon); - tray.setToolTip("ente"); - tray.setContextMenu(buildContextMenu(mainWindow)); - return tray; -} - -export function handleDownloads(mainWindow: BrowserWindow) { - mainWindow.webContents.session.on("will-download", (_, item) => { - item.setSavePath( - getUniqueSavePath(item.getFilename(), app.getPath("downloads")), - ); - }); -} - -export function handleExternalLinks(mainWindow: BrowserWindow) { - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (!url.startsWith(rendererURL)) { - require("electron").shell.openExternal(url); - return { action: "deny" }; - } else { - return { action: "allow" }; - } - }); -} - -export function getUniqueSavePath(filename: string, directory: string): string { - let uniqueFileSavePath = path.join(directory, filename); - const { name: filenameWithoutExtension, ext: extension } = - path.parse(filename); - let n = 0; - while (existsSync(uniqueFileSavePath)) { - n++; - // filter need to remove undefined extension from the array - // else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string - const fileNameWithNumberedSuffix = [ - `${filenameWithoutExtension}(${n})`, - extension, - ] - .filter((x) => x) // filters out undefined/null values - .join(""); - uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); - } - return uniqueFileSavePath; -} - -export function setupMacWindowOnDockIconClick() { - app.on("activate", function () { - const windows = BrowserWindow.getAllWindows(); - // we allow only one window - windows[0].show(); - }); -} - -export async function setupMainMenu(mainWindow: BrowserWindow) { - Menu.setApplicationMenu(await buildMenuBar(mainWindow)); -} - -export async function handleDockIconHideOnAutoLaunch() { - const shouldHideDockIcon = getHideDockIconPreference(); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - - if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { - app.dock.hide(); - } -} - -export function enableSharedArrayBufferSupport() { - app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); -} - -export function logSystemInfo() { - const systemVersion = process.getSystemVersion(); - const osName = process.platform; - const osRelease = os.release(); - ElectronLog.info({ osName, osRelease, systemVersion }); - const appVersion = app.getVersion(); - ElectronLog.info({ appVersion }); -} - -export async function checkIfInstalledViaBrew() { - if (!isPlatform("mac")) { - return false; - } - try { - await execAsync("brew list --cask ente"); - ElectronLog.info("ente installed via brew"); - return true; - } catch (e) { - ElectronLog.info("ente not installed via brew"); - return false; - } -} diff --git a/desktop/src/utils/menu.ts b/desktop/src/utils/menu.ts deleted file mode 100644 index c86786ff6..000000000 --- a/desktop/src/utils/menu.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { - app, - BrowserWindow, - Menu, - MenuItemConstructorOptions, - shell, -} from "electron"; -import ElectronLog from "electron-log"; -import { setIsAppQuitting } from "../main"; -import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; -import autoLauncher from "../services/autoLauncher"; -import { - getHideDockIconPreference, - setHideDockIconPreference, -} from "../services/userPreference"; -import { isPlatform } from "./common/platform"; - -export function buildContextMenu(mainWindow: BrowserWindow): Menu { - // eslint-disable-next-line camelcase - const contextMenu = Menu.buildFromTemplate([ - { - label: "Open ente", - click: function () { - mainWindow.maximize(); - mainWindow.show(); - }, - }, - { - label: "Quit ente", - click: function () { - ElectronLog.log("user quit the app"); - setIsAppQuitting(true); - app.quit(); - }, - }, - ]); - return contextMenu; -} - -export async function buildMenuBar(mainWindow: BrowserWindow): Promise { - let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - const isMac = isPlatform("mac"); - let shouldHideDockIcon = getHideDockIconPreference(); - const template: MenuItemConstructorOptions[] = [ - { - label: "ente", - submenu: [ - ...((isMac - ? [ - { - label: "About ente", - role: "about", - }, - ] - : []) as MenuItemConstructorOptions[]), - { type: "separator" }, - { - label: "Check for updates...", - click: () => { - forceCheckForUpdateAndNotify(mainWindow); - }, - }, - { - label: "View Changelog", - click: () => { - shell.openExternal( - "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", - ); - }, - }, - { type: "separator" }, - - { - label: "Preferences", - submenu: [ - { - label: "Open ente on startup", - type: "checkbox", - checked: isAutoLaunchEnabled, - click: () => { - autoLauncher.toggleAutoLaunch(); - isAutoLaunchEnabled = !isAutoLaunchEnabled; - }, - }, - { - label: "Hide dock icon", - type: "checkbox", - checked: shouldHideDockIcon, - click: () => { - setHideDockIconPreference(!shouldHideDockIcon); - shouldHideDockIcon = !shouldHideDockIcon; - }, - }, - ], - }, - - { type: "separator" }, - ...((isMac - ? [ - { - label: "Hide ente", - role: "hide", - }, - { - label: "Hide others", - role: "hideOthers", - }, - ] - : []) as MenuItemConstructorOptions[]), - - { type: "separator" }, - { - label: "Quit ente", - role: "quit", - }, - ], - }, - { - label: "Edit", - submenu: [ - { role: "undo", label: "Undo" }, - { role: "redo", label: "Redo" }, - { type: "separator" }, - { role: "cut", label: "Cut" }, - { role: "copy", label: "Copy" }, - { role: "paste", label: "Paste" }, - ...((isMac - ? [ - { - role: "pasteAndMatchStyle", - label: "Paste and match style", - }, - { role: "delete", label: "Delete" }, - { role: "selectAll", label: "Select all" }, - { type: "separator" }, - { - label: "Speech", - submenu: [ - { - role: "startSpeaking", - label: "start speaking", - }, - { - role: "stopSpeaking", - label: "stop speaking", - }, - ], - }, - ] - : [ - { type: "separator" }, - { role: "selectAll", label: "Select all" }, - ]) as MenuItemConstructorOptions[]), - ], - }, - { - label: "View", - submenu: [ - { role: "reload", label: "Reload" }, - { role: "forceReload", label: "Force reload" }, - { role: "toggleDevTools", label: "Toggle dev tools" }, - { type: "separator" }, - { role: "resetZoom", label: "Reset zoom" }, - { role: "zoomIn", label: "Zoom in" }, - { role: "zoomOut", label: "Zoom out" }, - { type: "separator" }, - { role: "togglefullscreen", label: "Toggle fullscreen" }, - ], - }, - { - label: "Window", - submenu: [ - { role: "close", label: "Close" }, - { role: "minimize", label: "Minimize" }, - ...((isMac - ? [ - { type: "separator" }, - { role: "front", label: "Bring to front" }, - { type: "separator" }, - { role: "window", label: "ente" }, - ] - : []) as MenuItemConstructorOptions[]), - ], - }, - { - label: "Help", - submenu: [ - { - label: "FAQ", - click: () => shell.openExternal("https://ente.io/faq/"), - }, - { type: "separator" }, - { - label: "Support", - click: () => shell.openExternal("mailto:support@ente.io"), - }, - { - label: "Product updates", - click: () => shell.openExternal("https://ente.io/blog/"), - }, - { type: "separator" }, - { - label: "View crash reports", - click: () => { - shell.openPath(app.getPath("crashDumps")); - }, - }, - { - label: "View logs", - click: () => { - shell.openPath(app.getPath("logs")); - }, - }, - ], - }, - ]; - return Menu.buildFromTemplate(template); -} diff --git a/desktop/src/utils/processStats.ts b/desktop/src/utils/processStats.ts deleted file mode 100644 index b10238eea..000000000 --- a/desktop/src/utils/processStats.ts +++ /dev/null @@ -1,295 +0,0 @@ -import ElectronLog from "electron-log"; -import { webFrame } from "electron/renderer"; -import { convertBytesToHumanReadable } from "./logging"; - -const LOGGING_INTERVAL_IN_MICROSECONDS = 30 * 1000; // 30 seconds - -const SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS = 1 * 1000; // 1 seconds - -const MAIN_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE = 50 * 1024; // 50 MB - -const HIGH_MAIN_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES = 200 * 1024; // 200 MB - -const RENDERER_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE = 200 * 1024; // 200 MB - -const HIGH_RENDERER_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES = 1024 * 1024; // 1 GB - -async function logMainProcessStats() { - const processMemoryInfo = await getNormalizedProcessMemoryInfo( - await process.getProcessMemoryInfo(), - ); - const cpuUsage = process.getCPUUsage(); - const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics(), - ); - - ElectronLog.log("main process stats", { - processMemoryInfo, - heapStatistics, - cpuUsage, - }); -} - -let previousMainProcessMemoryInfo: Electron.ProcessMemoryInfo = { - private: 0, - shared: 0, - residentSet: 0, -}; - -let mainProcessUsingHighMemory = false; - -async function logSpikeMainMemoryUsage() { - const processMemoryInfo = await process.getProcessMemoryInfo(); - const currentMemoryUsage = Math.max( - processMemoryInfo.residentSet ?? 0, - processMemoryInfo.private, - ); - const previousMemoryUsage = Math.max( - previousMainProcessMemoryInfo.residentSet ?? 0, - previousMainProcessMemoryInfo.private, - ); - const isSpiking = - currentMemoryUsage - previousMemoryUsage >= - MAIN_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE; - - const isHighMemoryUsage = - currentMemoryUsage >= HIGH_MAIN_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES; - - const shouldReport = - (isHighMemoryUsage && !mainProcessUsingHighMemory) || - (!isHighMemoryUsage && mainProcessUsingHighMemory); - - if (isSpiking || shouldReport) { - const normalizedCurrentProcessMemoryInfo = - await getNormalizedProcessMemoryInfo(processMemoryInfo); - const normalizedPreviousProcessMemoryInfo = - await getNormalizedProcessMemoryInfo(previousMainProcessMemoryInfo); - const cpuUsage = process.getCPUUsage(); - const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics(), - ); - - ElectronLog.log("reporting main memory usage spike", { - currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo, - previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo, - heapStatistics, - cpuUsage, - }); - } - previousMainProcessMemoryInfo = processMemoryInfo; - if (shouldReport) { - mainProcessUsingHighMemory = !mainProcessUsingHighMemory; - } -} - -let previousRendererProcessMemoryInfo: Electron.ProcessMemoryInfo = { - private: 0, - shared: 0, - residentSet: 0, -}; - -let rendererUsingHighMemory = false; - -async function logSpikeRendererMemoryUsage() { - const processMemoryInfo = await process.getProcessMemoryInfo(); - const currentMemoryUsage = Math.max( - processMemoryInfo.residentSet ?? 0, - processMemoryInfo.private, - ); - - const previousMemoryUsage = Math.max( - previousRendererProcessMemoryInfo.private, - previousRendererProcessMemoryInfo.residentSet ?? 0, - ); - const isSpiking = - currentMemoryUsage - previousMemoryUsage >= - RENDERER_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE; - - const isHighMemoryUsage = - currentMemoryUsage >= HIGH_RENDERER_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES; - - const shouldReport = - (isHighMemoryUsage && !rendererUsingHighMemory) || - (!isHighMemoryUsage && rendererUsingHighMemory); - - if (isSpiking || shouldReport) { - const normalizedCurrentProcessMemoryInfo = - await getNormalizedProcessMemoryInfo(processMemoryInfo); - const normalizedPreviousProcessMemoryInfo = - await getNormalizedProcessMemoryInfo( - previousRendererProcessMemoryInfo, - ); - const cpuUsage = process.getCPUUsage(); - const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics(), - ); - - ElectronLog.log("reporting renderer memory usage spike", { - currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo, - previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo, - heapStatistics, - cpuUsage, - }); - } - previousRendererProcessMemoryInfo = processMemoryInfo; - if (shouldReport) { - rendererUsingHighMemory = !rendererUsingHighMemory; - } -} - -async function logRendererProcessStats() { - const blinkMemoryInfo = getNormalizedBlinkMemoryInfo(); - const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics(), - ); - const webFrameResourceUsage = getNormalizedWebFrameResourceUsage(); - const processMemoryInfo = await getNormalizedProcessMemoryInfo( - await process.getProcessMemoryInfo(), - ); - ElectronLog.log("renderer process stats", { - blinkMemoryInfo, - heapStatistics, - processMemoryInfo, - webFrameResourceUsage, - }); -} - -export function setupMainProcessStatsLogger() { - setInterval( - logSpikeMainMemoryUsage, - SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS, - ); - setInterval(logMainProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS); -} - -export function setupRendererProcessStatsLogger() { - setInterval( - logSpikeRendererMemoryUsage, - SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS, - ); - setInterval(logRendererProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS); -} - -export async function logRendererProcessMemoryUsage(message: string) { - const processMemoryInfo = await process.getProcessMemoryInfo(); - const processMemory = Math.max( - processMemoryInfo.private, - processMemoryInfo.residentSet ?? 0, - ); - ElectronLog.log( - "renderer ProcessMemory", - message, - convertBytesToHumanReadable(processMemory * 1024), - ); -} - -const getNormalizedProcessMemoryInfo = async ( - processMemoryInfo: Electron.ProcessMemoryInfo, -) => { - return { - residentSet: convertBytesToHumanReadable( - processMemoryInfo.residentSet * 1024, - ), - private: convertBytesToHumanReadable(processMemoryInfo.private * 1024), - shared: convertBytesToHumanReadable(processMemoryInfo.shared * 1024), - }; -}; - -const getNormalizedBlinkMemoryInfo = () => { - const blinkMemoryInfo = process.getBlinkMemoryInfo(); - return { - allocated: convertBytesToHumanReadable( - blinkMemoryInfo.allocated * 1024, - ), - total: convertBytesToHumanReadable(blinkMemoryInfo.total * 1024), - }; -}; - -const getNormalizedHeapStatistics = ( - heapStatistics: Electron.HeapStatistics, -) => { - return { - totalHeapSize: convertBytesToHumanReadable( - heapStatistics.totalHeapSize * 1024, - ), - totalHeapSizeExecutable: convertBytesToHumanReadable( - heapStatistics.totalHeapSizeExecutable * 1024, - ), - totalPhysicalSize: convertBytesToHumanReadable( - heapStatistics.totalPhysicalSize * 1024, - ), - totalAvailableSize: convertBytesToHumanReadable( - heapStatistics.totalAvailableSize * 1024, - ), - usedHeapSize: convertBytesToHumanReadable( - heapStatistics.usedHeapSize * 1024, - ), - - heapSizeLimit: convertBytesToHumanReadable( - heapStatistics.heapSizeLimit * 1024, - ), - mallocedMemory: convertBytesToHumanReadable( - heapStatistics.mallocedMemory * 1024, - ), - peakMallocedMemory: convertBytesToHumanReadable( - heapStatistics.peakMallocedMemory * 1024, - ), - doesZapGarbage: heapStatistics.doesZapGarbage, - }; -}; - -const getNormalizedWebFrameResourceUsage = () => { - const webFrameResourceUsage = webFrame.getResourceUsage(); - return { - images: { - count: webFrameResourceUsage.images.count, - size: convertBytesToHumanReadable( - webFrameResourceUsage.images.size, - ), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.images.liveSize, - ), - }, - scripts: { - count: webFrameResourceUsage.scripts.count, - size: convertBytesToHumanReadable( - webFrameResourceUsage.scripts.size, - ), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.scripts.liveSize, - ), - }, - cssStyleSheets: { - count: webFrameResourceUsage.cssStyleSheets.count, - size: convertBytesToHumanReadable( - webFrameResourceUsage.cssStyleSheets.size, - ), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.cssStyleSheets.liveSize, - ), - }, - xslStyleSheets: { - count: webFrameResourceUsage.xslStyleSheets.count, - size: convertBytesToHumanReadable( - webFrameResourceUsage.xslStyleSheets.size, - ), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.xslStyleSheets.liveSize, - ), - }, - fonts: { - count: webFrameResourceUsage.fonts.count, - size: convertBytesToHumanReadable(webFrameResourceUsage.fonts.size), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.fonts.liveSize, - ), - }, - other: { - count: webFrameResourceUsage.other.count, - size: convertBytesToHumanReadable(webFrameResourceUsage.other.size), - liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.other.liveSize, - ), - }, - }; -}; diff --git a/desktop/src/utils/temp.ts b/desktop/src/utils/temp.ts index 91496ce13..489e5cbd4 100644 --- a/desktop/src/utils/temp.ts +++ b/desktop/src/utils/temp.ts @@ -1,17 +1,14 @@ -import { app } from "electron"; +import { app } from "electron/main"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; import path from "path"; -import { existsSync, mkdir } from "promise-fs"; - -const ENTE_TEMP_DIRECTORY = "ente"; const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; export async function getTempDirPath() { - const tempDirPath = path.join(app.getPath("temp"), ENTE_TEMP_DIRECTORY); - if (!existsSync(tempDirPath)) { - await mkdir(tempDirPath); - } + const tempDirPath = path.join(app.getPath("temp"), "ente"); + await fs.mkdir(tempDirPath, { recursive: true }); return tempDirPath; } diff --git a/desktop/src/utils/watch.ts b/desktop/src/utils/watch.ts index d8575ebd7..b5bf13029 100644 --- a/desktop/src/utils/watch.ts +++ b/desktop/src/utils/watch.ts @@ -1,4 +1,4 @@ -import { WatchMapping } from "../types"; +import { WatchMapping } from "../types/ipc"; export function isMappingPresent( watchMappings: WatchMapping[], diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 142c36005..700ea3fa0 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -1,17 +1,74 @@ { + /* TSConfig for a set of vanilla TypeScript files that need to be transpiled + into JavaScript that'll then be loaded and run by the main (node) process + of our Electron app. */ + + /* TSConfig docs: https://aka.ms/tsconfig.json */ + "compilerOptions": { - "target": "es2021", - "module": "commonjs", + /* Recommended target, lib and other settings for code running in the + version of Node.js bundled with Electron. + + Currently, with Electron 29, this is Node.js 20.9 + https://www.electronjs.org/blog/electron-29-0 + + Note that we cannot do + + "extends": "@tsconfig/node20/tsconfig.json", + + because that sets "lib": ["es2023"]. However (and I don't fully + understand what's going on here), that breaks our compilation since + tsc can then not find type definitions of things like ReadableStream. + + Adding "dom" to "lib" (e.g. `"lib": ["es2023", "dom"]`) fixes the + issue, but that doesn't sound correct - the main Electron process + isn't running in a browser context. + + It is possible that we're using some of the types incorrectly. For + now, we just omit the "lib" definition and rely on the defaults for + the "target" we've chosen. This is also what the current + electron-forge starter does: + + yarn create electron-app electron-forge-starter -- --template=webpack-typescript + + Enhancement: Can revisit this later. + + Refs: + - https://github.com/electron/electron/issues/27092 + - https://github.com/electron/electron/issues/16146 + */ + + "target": "es2022", + "module": "node16", + + /* Enable various workarounds to play better with CJS libraries */ "esModuleInterop": true, - /* Emit the generated JS into app */ + /* Speed things up by not type checking `node_modules` */ + "skipLibCheck": true, + + /* Emit the generated JS into `app/` */ "outDir": "app", - "noImplicitAny": true, - "sourceMap": true, - "baseUrl": "src", - "paths": { - "*": ["node_modules/*"] - } + + /* Temporary overrides to get things to compile with the older config */ + "strict": false, + "noImplicitAny": true + + /* Below is the state we want */ + /* Enable these one by one */ + // "strict": true, + + /* Require the `type` modifier when importing types */ + // "verbatimModuleSyntax": true + + /* Stricter than strict */ + // "noImplicitReturns": true, + // "noUnusedParameters": true, + // "noUnusedLocals": true, + // "noFallthroughCasesInSwitch": true, + /* e.g. makes array indexing returns undefined */ + // "noUncheckedIndexedAccess": true, + // "exactOptionalPropertyTypes": true, }, - /* Transpile all ts files in src/ */ + /* Transpile all `.ts` files in `src/` */ "include": ["src/**/*.ts"] } diff --git a/desktop/yarn.lock b/desktop/yarn.lock index c8080d23f..a4cc12cfe 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -2,38 +2,45 @@ # yarn lockfile v1 -"7zip-bin@~5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.1.1.tgz#9274ec7460652f9c632c59addf24efb1684ef876" - integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ== +"7zip-bin@~5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.2.0.tgz#7a03314684dd6572b7dfa89e68ce31d60286854d" + integrity sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A== -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== "@babel/code-frame@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" -"@babel/helper-validator-identifier@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" - integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/runtime@^7.21.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + dependencies: + regenerator-runtime "^0.14.0" "@derhuerst/http-basic@^8.2.0": version "8.2.4" @@ -54,9 +61,9 @@ ajv-keywords "^3.4.1" "@electron/asar@^3.2.1": - version "3.2.7" - resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.7.tgz#bb8117dc6fd0c06a922ae7fb1c0e2d433e35a6e5" - integrity sha512-8FaSCAIiZGYFWyjeevPQt+0e9xCK9YmJ2Rjg5SXgdsXon6cRnU0Yxnbe6CvJbQn26baifur2Y2G5EBayRIsjyg== + version "3.2.9" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.9.tgz#7b3a1fd677b485629f334dd80ced8c85353ba7e7" + integrity sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA== dependencies: commander "^5.0.0" glob "^7.1.6" @@ -77,10 +84,10 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/notarize@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.1.0.tgz#76aaec10c8687225e8d0a427cc9df67611c46ff3" - integrity sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA== +"@electron/notarize@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.2.1.tgz#d0aa6bc43cba830c41bfd840b85dbe0e273f59fe" + integrity sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg== dependencies: debug "^4.1.1" fs-extra "^9.0.1" @@ -98,10 +105,10 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/universal@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.4.1.tgz#3fbda2a5ed9ff9f3304c8e8316b94c1e3a7b3785" - integrity sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ== +"@electron/universal@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.5.1.tgz#f338bc5bcefef88573cf0ab1d5920fac10d06ee5" + integrity sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw== dependencies: "@electron/asar" "^3.2.1" "@malept/cross-spawn-promise" "^1.1.0" @@ -111,34 +118,68 @@ minimatch "^3.0.4" plist "^3.0.4" -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.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/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" "@malept/cross-spawn-promise@^1.1.0": version "1.1.1" @@ -170,7 +211,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -178,10 +219,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@octetstream/promisify@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@octetstream/promisify/-/promisify-2.0.2.tgz#29ac3bd7aefba646db670227f895d812c1a19615" - integrity sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + 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" @@ -205,10 +246,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@types/auto-launch@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.2.tgz#4970f01e5dd27572489b7fe77590204a19f86bd0" - integrity sha512-b03X09+GCM9t6AUECpwA2gUPYs8s5tJHFJw92sK8EiJ7G4QNbsHmXV7nfCfP6G6ivtm230vi4oNfe8AzRgzxMQ== +"@types/auto-launch@^5.0": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.5.tgz#439ed36aaaea501e2e2cfbddd8a20c366c34863b" + integrity sha512-/nGvQZSzM/pvCMCh4Gt2kIeiUmOP/cKGJbjlInI+A+5MoV/7XmT56DJ6EU8bqc3+ItxEe4UC2GVspmPzcCc8cg== "@types/cacheable-request@^6.0.1": version "6.0.3" @@ -221,16 +262,16 @@ "@types/responselike" "^1.0.0" "@types/debug@^4.1.6": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== dependencies: "@types/ms" "*" -"@types/ffmpeg-static@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.1.tgz#1003f003624bcd2f569b56185a62dcbacd935c39" - integrity sha512-hEJdQMv/g1olk9qTiWqh23BfbKsDKE6Tc7DilNJWF1MgZsU9fYOPKrgQ448vfT7aP2Yt5re9vgJDVv9TXEoTyQ== +"@types/ffmpeg-static@^3.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.3.tgz#605358ac6304507a75c2fd5fd861534837b19e2f" + integrity sha512-wmjANN0CiYs5clQESK+xE6plet0y9ndqaNBdQx4IIw7ZbPBMQw+14Lq4ky2WqMqGlpFJ9ZUxU0O43TvVZziyyA== "@types/fs-extra@9.0.13", "@types/fs-extra@^9.0.11": version "9.0.13" @@ -239,20 +280,15 @@ dependencies: "@types/node" "*" -"@types/get-folder-size@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/get-folder-size/-/get-folder-size-2.0.0.tgz#acbb5bf5999410c375b2739863a9d2f9483fabf6" - integrity sha512-6VKKrDB20E/6ovi2Pfpy9Pcz8Me1ue/tReaZrwrz9mfVdsr6WAMiDZ+F1oAAcss4U5n2k673i1leDIx2aEBDFQ== - "@types/http-cache-semantics@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz#abe102d06ccda1efdf0ed98c10ccf7f36a785a41" - integrity sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== -"@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/keyv@^3.1.4": version "3.1.4" @@ -262,176 +298,164 @@ "@types/node" "*" "@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node-fetch@^2.6.2": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== +"@types/node@*", "@types/node@^20.9.0": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node@*": - version "18.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" - integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== - -"@types/node@18.15.0": - version "18.15.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.0.tgz#286a65e3fdffd691e170541e6ecb0410b16a38be" - integrity sha512-z6nr0TTEOBGkzLGmbypWOGnpSpSIBorEhC4L+4HeQ2iezKCi4f77kyslRwvHeNitymGQ+oFyIWGP96l/DPSV9w== + undici-types "~5.26.4" "@types/node@^10.0.3": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^18.11.18": - version "18.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763" - integrity sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw== - "@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/plist@^3.0.1": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" - integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" + integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== dependencies: "@types/node" "*" xmlbuilder ">=11.0.1" -"@types/promise-fs@^2.1.1": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/promise-fs/-/promise-fs-2.1.2.tgz#7ef6ab00c7fbc68081e34e560d2f008d3dd27fd2" - integrity sha512-s3YON1LmplAUVrvTT2d1I0m2Rk0hSgc/1l5/krnU96YpP4NG9VEN/qopaFv8yk5a2Z+AgYzafS1LCP+kQH0MYw== - dependencies: - "@types/node" "*" - "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== dependencies: "@types/node" "*" -"@types/semver@^7.3.6": - version "7.3.10" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.10.tgz#5f19ee40cbeff87d916eedc8c2bfe2305d957f73" - integrity sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/verror@^1.10.3": - version "1.10.5" - resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1" - integrity sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw== + version "1.10.10" + resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087" + integrity sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg== "@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.28.0": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz#9c6017b6c1d04894141b4a87816388967f64c359" - integrity sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg== +"@typescript-eslint/eslint-plugin@^7": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328" + integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw== dependencies: - "@typescript-eslint/scope-manager" "5.30.6" - "@typescript-eslint/type-utils" "5.30.6" - "@typescript-eslint/utils" "5.30.6" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/type-utils" "7.4.0" + "@typescript-eslint/utils" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" - functional-red-black-tree "^1.0.1" - ignore "^5.2.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.28.0": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.6.tgz#add440db038fa9d777e4ebdaf66da9e7fb7abe92" - integrity sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA== +"@typescript-eslint/parser@^7": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1" + integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ== dependencies: - "@typescript-eslint/scope-manager" "5.30.6" - "@typescript-eslint/types" "5.30.6" - "@typescript-eslint/typescript-estree" "5.30.6" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz#ce1b49ff5ce47f55518d63dbe8fc9181ddbd1a33" - integrity sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g== +"@typescript-eslint/scope-manager@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f" + integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw== dependencies: - "@typescript-eslint/types" "5.30.6" - "@typescript-eslint/visitor-keys" "5.30.6" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" -"@typescript-eslint/type-utils@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz#a64aa9acbe609ab77f09f53434a6af2b9685f3af" - integrity sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA== +"@typescript-eslint/type-utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd" + integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw== dependencies: - "@typescript-eslint/utils" "5.30.6" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/utils" "7.4.0" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" -"@typescript-eslint/types@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.6.tgz#86369d0a7af8c67024115ac1da3e8fb2d38907e1" - integrity sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg== +"@typescript-eslint/types@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6" + integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw== -"@typescript-eslint/typescript-estree@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz#a84a0d6a486f9b54042da1de3d671a2c9f14484e" - integrity sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A== +"@typescript-eslint/typescript-estree@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4" + integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg== dependencies: - "@typescript-eslint/types" "5.30.6" - "@typescript-eslint/visitor-keys" "5.30.6" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/utils@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.6.tgz#1de2da14f678e7d187daa6f2e4cdb558ed0609dc" - integrity sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA== +"@typescript-eslint/utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd" + integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg== dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.30.6" - "@typescript-eslint/types" "5.30.6" - "@typescript-eslint/typescript-estree" "5.30.6" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" + semver "^7.5.4" -"@typescript-eslint/visitor-keys@5.30.6": - version "5.30.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz#94dd10bb481c8083378d24de1742a14b38a2678c" - integrity sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA== +"@typescript-eslint/visitor-keys@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94" + integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA== dependencies: - "@typescript-eslint/types" "5.30.6" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "7.4.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@xmldom/xmldom@^0.8.8": version "0.8.10" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== agent-base@6: version "6.0.2" @@ -452,7 +476,7 @@ ajv-keywords@^3.4.1: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -462,31 +486,26 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== +ajv@^8.0.0, ajv@^8.6.3: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -501,15 +520,20 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -any-shell-escape@^0.1.1: +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-shell-escape@^0.1: version "0.1.1" resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ== anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -519,26 +543,25 @@ app-builder-bin@4.0.0: resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0" integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA== -app-builder-lib@24.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.6.4.tgz#5bf77dd89d3ee557bc615b9ddfaf383f3e51577b" - integrity sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg== +app-builder-lib@24.13.3: + version "24.13.3" + resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.13.3.tgz#36e47b65fecb8780bb73bff0fee4e0480c28274b" + integrity sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig== dependencies: - "7zip-bin" "~5.1.1" "@develar/schema-utils" "~2.6.5" - "@electron/notarize" "2.1.0" + "@electron/notarize" "2.2.1" "@electron/osx-sign" "1.0.5" - "@electron/universal" "1.4.1" + "@electron/universal" "1.5.1" "@malept/flatpak-bundler" "^0.4.0" "@types/fs-extra" "9.0.13" async-exit-hook "^2.0.1" bluebird-lst "^1.0.9" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chromium-pickle-js "^0.2.0" debug "^4.3.4" ejs "^3.1.8" - electron-publish "24.5.0" + electron-publish "24.13.1" form-data "^4.0.0" fs-extra "^10.1.0" hosted-git-info "^4.1.0" @@ -575,14 +598,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: +assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== @@ -598,9 +614,9 @@ async-exit-hook@^2.0.1: integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" @@ -617,10 +633,10 @@ atomically@^1.7.0: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== -auto-launch@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.5.tgz#d14bd002b1ef642f85e991a6195ff5300c8ad3c0" - integrity sha512-ppdF4mihhYzMYLuCcx9H/c5TUOCev8uM7en53zWVQhyYAJrurd2bFZx3qQVeJKF2jrc7rsPRNN5cD+i23l6PdA== +auto-launch@^5.0: + version "5.0.6" + resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.6.tgz#ccc238ddc07b2fa84e96a1bc2fd11b581a20cb2d" + integrity sha512-OgxiAm4q9EBf9EeXdPBiVNENaWE3jUZofwrhAkWjHDYGezu1k3FRZHU8V2FBxGuSJOHzKmTJEd0G7L7/0xDGFA== dependencies: applescript "^1.0.0" mkdirp "^0.5.1" @@ -628,16 +644,6 @@ auto-launch@^5.0.5: untildify "^3.0.2" winreg "1.2.4" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -648,17 +654,10 @@ base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bluebird-lst@^1.0.9: version "1.0.9" @@ -722,32 +721,32 @@ buffer@^5.1.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builder-util-runtime@8.9.2: - version "8.9.2" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz#a9669ae5b5dcabfe411ded26678e7ae997246c28" - integrity sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A== - dependencies: - debug "^4.3.2" - sax "^1.2.4" - -builder-util-runtime@9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz#3184dcdf7ed6c47afb8df733813224ced4f624fd" - integrity sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA== +builder-util-runtime@9.2.3: + version "9.2.3" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" + integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== dependencies: debug "^4.3.4" sax "^1.2.4" -builder-util@24.5.0: - version "24.5.0" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.5.0.tgz#8683c9a7a1c5c9f9a4c4d2789ecca0e47dddd3f9" - integrity sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ== +builder-util-runtime@9.2.4: + version "9.2.4" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" + integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA== dependencies: - "7zip-bin" "~5.1.1" + debug "^4.3.4" + sax "^1.2.4" + +builder-util@24.13.1: + version "24.13.1" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816" + integrity sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA== + dependencies: + "7zip-bin" "~5.2.0" "@types/debug" "^4.1.6" app-builder-bin "4.0.0" bluebird-lst "^1.0.9" - builder-util-runtime "9.2.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" cross-spawn "^7.0.3" debug "^4.3.4" @@ -783,12 +782,12 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -caseless@^0.12.0, caseless@~0.12.0: +caseless@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^2.0.0: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -797,7 +796,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -805,10 +804,10 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -831,9 +830,9 @@ chromium-pickle-js@^0.2.0: integrity sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw== ci-info@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" - integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cli-truncate@^2.1.0: version "2.1.0" @@ -843,15 +842,6 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -862,17 +852,12 @@ cliui@^8.0.1: wrap-ansi "^7.0.0" clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== dependencies: mimic-response "^1.0.0" -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -897,7 +882,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -914,7 +899,7 @@ compare-version@^0.1.2: resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" integrity sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A== -compare-versions@^6.1.0: +compare-versions@^6.1: version "6.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== @@ -934,25 +919,25 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -concurrently@^7.0.0: - version "7.2.2" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.2.2.tgz#4ad4a4dfd3945f668d727379de2a29502e6a531c" - integrity sha512-DcQkI0ruil5BA/g7Xy3EWySGrFJovF5RYAYxwGvv9Jf9q9B1v3jPFP2tl6axExNf1qgF30kjoNYrangZ0ey4Aw== +concurrently@^8: + version "8.2.2" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784" + integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg== dependencies: - chalk "^4.1.0" - date-fns "^2.16.1" + chalk "^4.1.2" + date-fns "^2.30.0" lodash "^4.17.21" - rxjs "^7.0.0" - shell-quote "^1.7.3" - spawn-command "^0.0.2-1" - supports-color "^8.1.0" + rxjs "^7.8.1" + shell-quote "^1.8.1" + spawn-command "0.0.2" + supports-color "^8.1.1" tree-kill "^1.2.2" - yargs "^17.3.1" + yargs "^17.7.2" -conf@^10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/conf/-/conf-10.1.2.tgz#50132158f388756fa9dea3048f6b47935315c14e" - integrity sha512-o9Fv1Mv+6A0JpoayQ8JleNp3hhkbOJP/Re/Q+QqxMPHPkABVsRjQGWZn9A5GcqLiTNC6d89p2PB5ZhHVDSMwyg== +conf@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/conf/-/conf-10.2.0.tgz#838e757be963f1a2386dfe048a98f8f69f7b55d6" + integrity sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg== dependencies: ajv "^8.6.3" ajv-formats "^2.1.1" @@ -966,23 +951,18 @@ conf@^10.1.2: semver "^7.3.5" config-file-ts@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.4.tgz#6c0741fbe118a7cf786c65f139030f0448a2cc99" - integrity sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ== + version "0.2.6" + resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.6.tgz#b424ff74612fb37f626d6528f08f92ddf5d22027" + integrity sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w== dependencies: - glob "^7.1.6" - typescript "^4.0.2" + glob "^10.3.10" + typescript "^5.3.3" core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - crc@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" @@ -990,7 +970,7 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -999,17 +979,12 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== dependencies: - assert-plus "^1.0.0" - -date-fns@^2.16.1: - version "2.28.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" - integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + "@babel/runtime" "^7.21.0" debounce-fn@^4.0.0: version "4.0.0" @@ -1018,27 +993,13 @@ debounce-fn@^4.0.0: dependencies: mimic-fn "^3.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^2.1.3, debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.0.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -1046,11 +1007,6 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -1061,11 +1017,21 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-properties@^1.1.3: +define-data-property@^1.0.1: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -1104,14 +1070,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@24.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.6.4.tgz#e19b8305f7e1ea0b4faaa30382c81b9d6de39863" - integrity sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw== +dmg-builder@24.13.3: + version "24.13.3" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.13.3.tgz#95d5b99c587c592f90d168a616d7ec55907c7e55" + integrity sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ== dependencies: - app-builder-lib "24.6.4" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + app-builder-lib "24.13.3" + builder-util "24.13.1" + builder-util-runtime "9.2.4" fs-extra "^10.1.0" iconv-lite "^0.6.2" js-yaml "^4.1.0" @@ -1161,13 +1127,10 @@ dotenv@^9.0.2: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== ejs@^3.1.8: version "3.1.9" @@ -1176,26 +1139,26 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder-notarize@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.0.tgz#ef17c3f0eac6b3908ff2bc3568aea6e4f8612b5b" - integrity sha512-kbVCnZX3pCKTXiPhyoMjCZYSQnwS04QmlTM2NB2D/2LCsab5UJA0Me9ZqDT3W35ENPglf1WYDKT+tx9i+xuaPA== +electron-builder-notarize@^1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.1.tgz#e00b868a67ef20a77f00017606626f24fdbdc445" + integrity sha512-xS7s9gE+1AcJIuJ4DU/LqCrmRypE1zOR/6b66egKzgP/UVh9YSa7rINos34gF/KcueNDQU39HcXcCEKiEI5wPQ== dependencies: dotenv "^8.2.0" electron-notarize "^1.1.1" js-yaml "^3.14.0" read-pkg-up "^7.0.0" -electron-builder@^24.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.6.4.tgz#c51271e49b9a02c9a3ec444f866b6008c4d98a1d" - integrity sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA== +electron-builder@^24: + version "24.13.3" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.13.3.tgz#c506dfebd36d9a50a83ee8aa32d803d83dbe4616" + integrity sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg== dependencies: - app-builder-lib "24.6.4" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + app-builder-lib "24.13.3" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" - dmg-builder "24.6.4" + dmg-builder "24.13.3" fs-extra "^10.1.0" is-ci "^3.0.0" lazy-val "^1.0.5" @@ -1203,83 +1166,61 @@ electron-builder@^24.6.4: simple-update-notifier "2.0.0" yargs "^17.6.2" -electron-download@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" - integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg== - dependencies: - debug "^3.0.0" - env-paths "^1.0.0" - fs-extra "^4.0.1" - minimist "^1.2.0" - nugget "^2.0.1" - path-exists "^3.0.0" - rc "^1.2.1" - semver "^5.4.1" - sumchecker "^2.0.2" - -electron-log@^4.3.5: - version "4.4.8" - resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-4.4.8.tgz#fcb9f714dbcaefb6ac7984c4683912c74730248a" - integrity sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA== +electron-log@^5.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.2.tgz#fb40ad7f4ae694dd0e4c02c662d1a65c03e1243e" + integrity sha512-Cpg4hAZ27yM9wzE77c4TvgzxzavZ+dVltCczParXN+Vb3jocojCSAuSMCVOI9fhFuuOR+iuu3tZLX1cu0y0kgQ== electron-notarize@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.2.1.tgz#347c18eca8e29dddadadee511b870c13d4008baf" - integrity sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.2.2.tgz#ebf2b258e8e08c1c9f8ff61dc53d5b16b439daf4" + integrity sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw== dependencies: debug "^4.1.1" fs-extra "^9.0.1" -electron-publish@24.5.0: - version "24.5.0" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.5.0.tgz#492a4d7caa232e88ee3c18f5c3b4dc637e5e1b3a" - integrity sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA== +electron-publish@24.13.1: + version "24.13.1" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.13.1.tgz#57289b2f7af18737dc2ad134668cdd4a1b574a0c" + integrity sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" fs-extra "^10.1.0" lazy-val "^1.0.5" mime "^2.5.2" -electron-reload@^2.0.0-alpha.1: - version "2.0.0-alpha.1" - resolved "https://registry.yarnpkg.com/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz#6cad98df96695ca1d5462dc9407f7c620028ce99" - integrity sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g== +electron-store@^8.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1" + integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw== dependencies: - chokidar "^3.5.2" + conf "^10.2.0" + type-fest "^2.17.0" -electron-store@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.2.tgz#95c8cf81c1e1cf48b24f3ceeea24b921c1ff62d7" - integrity sha512-9GwUMv51w8ydbkaG7X0HrPlElXLApg63zYy1/VZ/a08ndl0gfm4iCoD3f0E1JvP3V16a+7KxqriCI0c122stiA== +electron-updater@^6.1: + version "6.1.8" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" + integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== dependencies: - conf "^10.1.2" - type-fest "^2.12.2" - -electron-updater@^4.3.8: - version "4.6.5" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.6.5.tgz#e9a75458bbfd6bb41a58a829839e150ad2eb2d3d" - integrity sha512-kdTly8O9mSZfm9fslc1mnCY+mYOeaYRy7ERa2Fed240u01BKll3aiupzkd07qKw69KvhBSzuHroIW3mF0D8DWA== - dependencies: - "@types/semver" "^7.3.6" - builder-util-runtime "8.9.2" - fs-extra "^10.0.0" + builder-util-runtime "9.2.3" + fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" lodash.escaperegexp "^4.1.2" lodash.isequal "^4.5.0" - semver "^7.3.5" + semver "^7.3.8" + tiny-typed-emitter "^2.1.0" -electron@^25.8.4: - version "25.8.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-25.8.4.tgz#b50877aac7d96323920437baf309ad86382cb455" - integrity sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg== +electron@^29: + version "29.1.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.5.tgz#b745b4d201c1ac9f84d6aa034126288dde34d5a1" + integrity sha512-1uWGRw/ffA62lcrklxGUgVxVtOHojsg/nwsYr+/F9cVjipZJn8iPv/ABGIIexhmUqWcho8BqfTJ4osCBa29gBg== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^18.11.18" + "@types/node" "^20.9.0" extract-zip "^2.0.1" emoji-regex@^8.0.0: @@ -1287,6 +1228,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1294,18 +1240,6 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -env-paths@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" - integrity sha512-+6r/UAzikJWJPcQZpBQS+bVmjAMz2BkDP/N4n2Uz1zz8lyw1IHWUeVdh/85gs0dp5A+z76LOQhCZkR6F88mlUw== - env-paths@^2.2.0, env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -1323,15 +1257,27 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es6-error@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-string-regexp@^1.0.5: version "1.0.5" @@ -1343,117 +1289,81 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-google@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" - integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== - -eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" - estraverse "^4.1.1" + estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, 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== + +eslint@^8: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^7.23.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" @@ -1464,11 +1374,6 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" @@ -1479,11 +1384,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" @@ -1495,11 +1395,6 @@ extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - extsprintf@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" @@ -1510,18 +1405,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.3.0: +fast-glob@^3.2.9, fast-glob@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -1543,9 +1427,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -1556,10 +1440,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -ffmpeg-static@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.1.0.tgz#133500f4566570c5a0e96795152b0526d8c936ad" - integrity sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg== +ffmpeg-static@^5.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz#6ca64a5ed6e69ec4896d175c1f69dd575db7c5ef" + integrity sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA== dependencies: "@derhuerst/http-basic" "^8.2.0" env-paths "^2.2.0" @@ -1573,7 +1457,7 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -filelist@^1.0.1: +filelist@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== @@ -1602,6 +1486,14 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -1615,19 +1507,13 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" + cross-spawn "^7.0.0" + signal-exit "^4.0.1" form-data@^4.0.0: version "4.0.0" @@ -1638,15 +1524,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - fs-extra@^10.0.0, fs-extra@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -1656,15 +1533,6 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1697,46 +1565,30 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - -gar@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/gar/-/gar-1.0.4.tgz#f777bc7db425c0572fdeb52676172ca1ae9888b8" - integrity sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-folder-size@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/get-folder-size/-/get-folder-size-2.0.1.tgz#3fe0524dd3bad05257ef1311331417bcd020a497" - integrity sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - gar "^1.0.4" - tiny-each-async "2.0.3" - -get-intrinsic@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" - integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-stdin@^9.0.0: version "9.0.0" @@ -1750,13 +1602,6 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.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" @@ -1769,7 +1614,25 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.1.3, glob@^7.1.6: +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^10.3.10: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^7.0.0, 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== @@ -1793,10 +1656,10 @@ global-agent@^3.0.0: semver "^7.3.2" serialize-error "^7.0.1" -globals@^13.6.0, globals@^13.9.0: - version "13.16.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.16.0.tgz#9be4aca28f311aaeb974ea54978ebbb5e35ce46a" - integrity sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -1830,6 +1693,13 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^11.8.5: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" @@ -1847,23 +1717,15 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== has-flag@^3.0.0: version "3.0.0" @@ -1876,23 +1738,28 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" hosted-git-info@^2.1.4: version "2.8.9" @@ -1906,15 +1773,15 @@ hosted-git-info@^4.1.0: dependencies: lru-cache "^6.0.0" -html-entities@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" - integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== +html-entities@^2.5: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-proxy-agent@^5.0.0: version "5.0.0" @@ -1932,15 +1799,6 @@ http-response-object@^3.0.1: dependencies: "@types/node" "^10.0.3" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -1977,22 +1835,12 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -ignore@^5.2.4: +ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -2013,15 +1861,15 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.1: +inherits@2, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== is-arrayish@^0.2.1: version "0.2.1" @@ -2042,25 +1890,18 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== - dependencies: - number-is-nan "^1.0.0" - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -2083,52 +1924,51 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^4.1.0: version "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-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== isbinaryfile@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.0.tgz#034b7e54989dab8986598cbcea41f66663c65234" - integrity sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg== + version "5.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.2.tgz#fe6e4dfe2e34e947ffa240c113444876ba393ae0" + integrity sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + version "10.8.7" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== dependencies: async "^3.2.3" chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" + filelist "^1.0.4" + minimatch "^3.1.2" -jpeg-js@^0.4.4: +jpeg-js@^0.4: version "0.4.4" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== @@ -2138,7 +1978,7 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.14.0: +js-yaml@^3.14.0: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -2153,11 +1993,6 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -2183,25 +2018,20 @@ json-schema-typed@^7.0.3: resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9" integrity sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" @@ -2219,20 +2049,10 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - keyv@^4.0.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -2269,6 +2089,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" @@ -2284,11 +2111,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2306,6 +2128,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -2331,7 +2158,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -2363,36 +2190,36 @@ 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@^3.0.4, minimatch@^3.1.1: +minimatch@9.0.3, minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" - integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^5.1.1: +minimatch@^5.0.1, minimatch@^5.1.1: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== minipass@^3.0.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" - integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" @@ -2401,6 +2228,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -2421,27 +2253,17 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next-electron-server@^1.0.0: +next-electron-server@^1: version "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== @@ -2451,14 +2273,7 @@ node-addon-api@^1.6.3: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-stream-zip@^1.15.0: +node-stream-zip@^1.15: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== @@ -2483,39 +2298,11 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -nugget@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.2.tgz#398b591377b740b3dd308fabecd5ea09cf3443da" - integrity sha512-A8A8+PtlH937KWXJnfct6ubGPfgHOe3lwFkkmrT5xW8+aRBnDWqSiW5NRuiVuh/k/auLGsZdu+WrIU2epL/FHg== - dependencies: - debug "^2.1.3" - minimist "^1.1.0" - pretty-bytes "^4.0.2" - progress-stream "^1.1.0" - request "^2.45.0" - single-line-log "^1.1.2" - throttleit "0.0.2" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-keys@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" - integrity sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw== - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2530,29 +2317,29 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onnxruntime-common@~1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.16.3.tgz#216bd1318d171496f1e92906a801c95bd2fb1aaa" - integrity sha512-ZZfFzEqBf6YIGwB9PtBLESHI53jMXA+/hn+ACVUbEfPuK2xI5vMGpLPn+idpwCmHsKJNRzRwqV12K+6TQj6tug== +onnxruntime-common@1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz#b2534ce021b1c1b19182bec39aaea8d547d2013e" + integrity sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw== -onnxruntime-node@^1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.16.3.tgz#8530439f4a513b17e4d3df0073f54c4614a46070" - integrity sha512-6T2pjwg5ik74VnI1IXFzxvPAm2UCo+vNNsDGbMP+A2q6GZPMYai2pMA17g3YMUvgOZLwsjWBUwNIlP4QaVRFlA== +onnxruntime-node@^1.17: + version "1.17.0" + resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz#38af0ba527cb44c1afb639bdcb4e549edba029a1" + integrity sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg== dependencies: - onnxruntime-common "~1.16.3" + onnxruntime-common "1.17.0" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" p-cancelable@^2.0.0: version "2.1.1" @@ -2566,6 +2353,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -2580,6 +2374,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -2632,6 +2433,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -2642,21 +2451,16 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -2664,15 +2468,7 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -plist@^3.0.4: - version "3.0.6" - resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" - integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== - dependencies: - base64-js "^1.5.1" - xmlbuilder "^15.1.1" - -plist@^3.0.5: +plist@^3.0.4, plist@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== @@ -2704,31 +2500,11 @@ prettier@^3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== -pretty-bytes@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" - integrity sha512-yJAF+AjbHKlxQ8eezMd/34Mnj/YTQ3i6kLzvVsH4l/BfIFtp444n0wVbnsn66JimZ9uBofv815aRp1zCppxlWw== - -progress-stream@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" - integrity sha512-MIBPjZz6oGNSw5rn2mSp+nP9FGoaVo6QsPyPVEaD4puilz5hZNa3kfnrlqRNYFsugslbU3An4mnkLLtZOaWvrA== - dependencies: - speedometer "~0.1.2" - through2 "~0.2.3" - -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/promise-fs/-/promise-fs-2.1.1.tgz#0b725a592c165ff16157d1f13640ba390637e557" - integrity sha512-43p7e4QzAQ3w6eyN0+gbBL7jXiZFWLWYITg9wIObqkBySu/a5K1EDcQ/S6UyB/bmiZWDA4NjTbcopKLTaKcGSw== - dependencies: - "@octetstream/promisify" "2.0.2" - promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -2737,11 +2513,6 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -psl@^1.1.28: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -2750,15 +2521,10 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" @@ -2770,16 +2536,6 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -rc@^1.2.1: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - read-config-file@6.3.2: version "6.3.2" resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-6.3.2.tgz#556891aa6ffabced916ed57457cb192e61880411" @@ -2812,24 +2568,14 @@ read-pkg@^5.2.0: type-fest "^0.6.0" readable-stream@^3.0.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -2837,36 +2583,17 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -regexpp@^3.1.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -request@^2.45.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" + resolve "^1.1.6" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== require-directory@^2.1.1: version "2.1.1" @@ -2888,12 +2615,12 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.10.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.1.6, resolve@^1.10.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -2914,7 +2641,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -2940,19 +2667,19 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.0.0: - version "7.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" - integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -2965,36 +2692,29 @@ sanitize-filename@^1.6.3: truncate-utf8-bytes "^1.0.0" sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -"semver@2 || 3 || 4 || 5", semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +"semver@2 || 3 || 4 || 5": + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8, semver@^7.5.3: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== +semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" @@ -3017,10 +2737,32 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" - integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== +shell-quote@^1.8.1: + version "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== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shx@^0.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" + integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== + dependencies: + minimist "^1.2.3" + shelljs "^0.8.5" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-update-notifier@2.0.0: version "2.0.0" @@ -3029,13 +2771,6 @@ simple-update-notifier@2.0.0: dependencies: semver "^7.5.3" -single-line-log@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" - integrity sha512-awzaaIPtYFdexLr6TBpcZSGPB6D1RInNO/qNetgaJloPDF/D0GkVtLvGEp8InfmLV7CyLyQ5fIRP+tVN/JmWQA== - dependencies: - string-width "^1.0.1" - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -3055,15 +2790,6 @@ slice-ansi@^3.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - smart-buffer@^4.0.2: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -3100,23 +2826,23 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg== +spawn-command@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -3127,55 +2853,26 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== - -speedometer@~0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" - integrity sha512-phdEoDlA6EUIVtzwq1UiNMXDUogczp204aYF/yfOhjNePWFfIpBJ1k5wLMuXQhEOOMjuTJEcc4vdZa+vuP+n/Q== + version "3.0.17" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" + integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== sprintf-js@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - stat-mode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3184,6 +2881,15 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -3191,42 +2897,25 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +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" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -sumchecker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" - integrity sha512-16O54scwFPgX60Of/+QJSufmklGqnHZyBK6uewBvtcp3VxT5RM65c/OnGCeEPnjBF8TJoO5Pf6gHAOXfxIjNpA== - dependencies: - debug "^2.2.0" - sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" @@ -3248,7 +2937,7 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.0: +supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -3268,21 +2957,10 @@ synckit@0.9.0: "@pkgr/core" "^0.1.0" tslib "^2.6.2" -table@^6.0.9: - version "6.8.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" - integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tar@^6.1.12: - version "6.2.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" - integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -3304,23 +2982,10 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -throttleit@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" - integrity sha512-HtlTFeyYs1elDM2txiIGsdXHaq8kffVaZH/QEBRbo95zQqzlsBx5ELKhkPOZVad9OK9oxzwx6UrQN8Vfh/+yag== - -through2@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" - integrity sha512-mLa8Bn2mZurjyomGKWRu3Bo2mvoQojFks9NvOK8H+k4kDJNkdEqG522KFZsEFBEl6rKkxTgFbE5+OPcgfvPEHA== - dependencies: - readable-stream "~1.1.9" - xtend "~2.1.1" - -tiny-each-async@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tiny-each-async/-/tiny-each-async-2.0.3.tgz#8ebbbfd6d6295f1370003fbb37162afe5a0a51d1" - integrity sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA== +tiny-typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" + integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== tmp-promise@^3.0.2: version "3.0.3" @@ -3330,11 +2995,9 @@ tmp-promise@^3.0.2: tmp "^0.2.0" tmp@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== to-regex-range@^5.0.1: version "5.0.1" @@ -3343,19 +3006,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -3368,10 +3018,10 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== tslib@^2.1.0: version "2.4.0" @@ -3383,25 +3033,6 @@ tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -3429,25 +3060,25 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^2.12.2: - version "2.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.16.0.tgz#1250fbd64dafaf4c8e405e393ef3fb16d9651db2" - integrity sha512-qpaThT2HQkFb83gMOrdKVsfCN7LKxP26Yq+smPzY1FqoHRjqmjqHXA7n5Gkxi8efirtbeEUxzfEdePthQWCuHw== +type-fest@^2.17.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^4.0.2: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5, typescript@^5.3.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== -typescript@^4.2.3: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== universalify@^0.1.0: version "0.1.2" @@ -3455,9 +3086,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== untildify@^3.0.2: version "3.0.3" @@ -3481,16 +3112,6 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -3499,15 +3120,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - verror@^1.10.0: version "1.10.1" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" @@ -3517,19 +3129,6 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3542,12 +3141,7 @@ winreg@1.2.4: resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA== -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -3556,6 +3150,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3566,13 +3169,6 @@ xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== -xtend@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" - integrity sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ== - dependencies: - object-keys "~0.4.0" - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -3583,30 +3179,12 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^21.0.0: - version "21.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" - integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1: - version "17.5.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" - integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - -yargs@^17.6.2: +yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -3626,3 +3204,8 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +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== diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index ca531716b..b7c3946a2 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -12,15 +12,32 @@ export const sidebar = [ items: [ { text: "Albums", link: "/photos/features/albums" }, { text: "Archiving", link: "/photos/features/archive" }, + { + text: "Background sync", + link: "/photos/features/background", + }, + { text: "Backup", link: "/photos/features/backup" }, { text: "Cast", link: "/photos/features/cast/" }, + { + text: "Collaboration", + link: "/photos/features/collaborate", + }, { text: "Collecting photos", link: "/photos/features/collect", }, + { + text: "Deduplicate", + link: "/photos/features/deduplicate", + }, { text: "Family plans", link: "/photos/features/family-plans", }, + { + text: "Free up space", + link: "/photos/features/free-up-space/", + }, { text: "Hidden photos", link: "/photos/features/hide" }, { text: "Location tags", @@ -32,20 +49,76 @@ export const sidebar = [ link: "/photos/features/public-link", }, { text: "Quick link", link: "/photos/features/quick-link" }, - { text: "Referrals", link: "/photos/features/referrals" }, - { text: "Sharing", link: "/photos/features/sharing" }, + { + text: "Referral program", + link: "/photos/features/referral-program/", + }, + { text: "Sharing", link: "/photos/features/share" }, { text: "Trash", link: "/photos/features/trash" }, { text: "Uncategorized", link: "/photos/features/uncategorized", }, { - text: "Watch folder", - link: "/photos/features/watch-folder", + text: "Watch folders", + link: "/photos/features/watch-folders", + }, + ], + }, + { + text: "Migration", + collapsed: true, + items: [ + { + text: "Introduction", + link: "/photos/migration/", + }, + + { + text: "From Google Photos", + link: "/photos/migration/from-google-photos/", + }, + { + text: "From Apple Photos", + link: "/photos/migration/from-apple-photos/", + }, + { + text: "From Amazon Photos", + link: "/photos/migration/from-amazon-photos", + }, + { + text: "From your hard disk", + link: "/photos/migration/from-local-hard-disk", + }, + { + text: "Exporting your data", + link: "/photos/migration/export/", + }, + ], + }, + { + text: "FAQ", + collapsed: true, + items: [ + { text: "General", link: "/photos/faq/general" }, + { + text: "Export", + link: "/photos/faq/export", + }, + { + text: "Security and privacy", + link: "/photos/faq/security-and-privacy", + }, + { + text: "Subscription and plans", + link: "/photos/faq/subscription", + }, + { + text: "Hide vs archive", + link: "/photos/faq/hidden-and-archive", }, ], }, - { text: "FAQ", link: "/photos/faq/" }, { text: "Troubleshooting", collapsed: true, @@ -68,8 +141,8 @@ export const sidebar = [ { text: "Introduction", link: "/auth/" }, { text: "FAQ", link: "/auth/faq/" }, { - text: "Migration guides", - collapsed: false, + text: "Migration", + collapsed: true, items: [ { text: "Introduction", link: "/auth/migration-guides/" }, { @@ -119,14 +192,11 @@ export const sidebar = [ { text: "FAQ", items: [ + { text: "General", link: "/self-hosting/faq/" }, { text: "Verification code", link: "/self-hosting/faq/otp", }, - { - text: "Increase storage space", - link: "/self-hosting/faq/storage-space", - }, ], }, { @@ -185,7 +255,7 @@ function sidebarOld() { }, { text: "Watch folder", - link: "/photos/features/watch-folder", + link: "/photos/features/watch-folders", }, { text: "Trash", link: "/photos/features/trash" }, { diff --git a/docs/docs/auth/migration-guides/authy/index.md b/docs/docs/auth/migration-guides/authy/index.md index d215a610b..48ce3965d 100644 --- a/docs/docs/auth/migration-guides/authy/index.md +++ b/docs/docs/auth/migration-guides/authy/index.md @@ -10,7 +10,7 @@ A guide written by Green, an ente.io lover > [!WARNING] > > Authy will soon be dropping support for its desktop apps in the near future. -> If you are looking to switch to ente Authenticator from Authy, I heavily +> If you are looking to switch to Ente Authenticator from Authy, I heavily > recommend you export your codes as soon as you can. --- @@ -19,7 +19,7 @@ Migrating from Authy can be tiring, as you cannot export your 2FA codes through the app, meaning that you would have to reconfigure 2FA for all of your accounts for your new 2FA authenticator. However, easier ways exist to export your codes out of Authy. This guide will cover two of the most used methods for mograting -from Authy to ente Authenticator. +from Authy to Ente Authenticator. > [!CAUTION] > @@ -39,7 +39,7 @@ hard (and rather technical) parts of the process.

One way to export is to [use this tool by Neeraj](https://github.com/ua741/authy-export/releases/tag/v0.0.4) -to simplify the process and skip directly to importing to ente Authenticator. +to simplify the process and skip directly to importing to Ente Authenticator. To export from Authy, download the tool for your specific OS, then type the following in your terminal: @@ -72,7 +72,7 @@ For Windows: ``` This will generate a text file called `authy_codes.txt`, which contains your -Authy codes in ente's plaintext export format. You can now import this to ente +Authy codes in Ente's plaintext export format. You can now import this to Ente Authenticator! ## Method 2: Use gboudreau's GitHub guide @@ -89,23 +89,23 @@ To export your data, please follow This will create a JSON file called `authy-to-bitwarden-export.json`, which contains your Authy codes in Bitwarden's export format. You can now import this -to ente Authenticator! +to Ente Authenticator! ### Method 2.1: If the export worked, but the import didn't > [!NOTE] -> -> This is intended only for users who successfully exported their codes using the -> guide in method 2, but could not import it to ente Authenticator for whatever -> reason. If the import was successful, or you haven't tried to import the codes -> yet, ignore this section. +> +> This is intended only for users who successfully exported their codes using +> the guide in method 2, but could not import it to Ente Authenticator for +> whatever reason. If the import was successful, or you haven't tried to import +> the codes yet, ignore this section. > > If the export itself failed, try using > [**method 1**](#method-1-use-neerajs-export-tool) instead. -Usually, you should be able to import Bitwarden exports directly into ente +Usually, you should be able to import Bitwarden exports directly into Ente Authenticator. In case this didn't work for whatever reason, I've written a -program in Python that converts the JSON file into a TXT file that ente +program in Python that converts the JSON file into a TXT file that Ente Authenticator can use, so you can try importing using plain text import instead. You can download my program @@ -140,18 +140,18 @@ To run the Python program, open it in your IDE and run the program, or open your terminal and type `python3 authy_to_ente.py` (MacOS/Linux, or any other OS that uses bash) or `py -3 authy_to_ente.py` (Windows). Once you run it, a new TXT file called `auth_codes.txt` will be generated. You can now import your data to -ente Authenticator! +Ente Authenticator! --- You should now have a TXT file (method 1, method 2.1) or a JSON file (method 2) -that countains your TOTP secrets, which can now be imported into ente +that countains your TOTP secrets, which can now be imported into Ente Authenticator. To import your codes, please follow one of the steps below, depending on which method you used to export your codes. -## Importing to ente Authenticator (Method 1, method 2.1) +## Importing to Ente Authenticator (Method 1, method 2.1) -1. Copy the TXT file to one of your devices with ente Authenticator. +1. Copy the TXT file to one of your devices with Ente Authenticator. 2. Log in to your account (if you haven't already), or press "Use without backups". 3. Open the navigation menu (hamburger button on the top left), then press @@ -159,9 +159,9 @@ depending on which method you used to export your codes. 4. Select the "Plain text" option. 5. Select the TXT file that was made earlier. -## Importing to ente Authenticator (Method 2) +## Importing to Ente Authenticator (Method 2) -1. Copy the JSON file to one of your devices with ente Authenticator. +1. Copy the JSON file to one of your devices with Ente Authenticator. 2. Log in to your account (if you haven't already), or press "Use without backups". 3. Open the navigation menu (hamburger button on the top left), then press @@ -172,7 +172,7 @@ depending on which method you used to export your codes. If this didn't work, refer to [**method 2.1**](#method-21-if-the-export-worked-but-the-import-didnt).

-And that's it! You have now successfully migrated from Authy to ente +And that's it! You have now successfully migrated from Authy to Ente Authenticator. Now that your secrets are safely stored, I recommend you delete the unencrypted diff --git a/docs/docs/photos/faq/export.md b/docs/docs/photos/faq/export.md new file mode 100644 index 000000000..ace1cd736 --- /dev/null +++ b/docs/docs/photos/faq/export.md @@ -0,0 +1,29 @@ +--- +title: Export FAQ +description: Frequently asked questions about keeping extra backups of your data +--- + +# Export + +## Can I backup my data in a local drive outside Ente? + +Yes! You can use our CLI tool or our desktop app to set up exports of your data +in a local drive or NAS of your choice. This way, you can use Ente in your day +to day use, but will have an additional guarantee that a copy of your original +photos and videos are always available in normal directories and files. + +* 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 + are incremental, and will also gracefully handle interruptions. + +* Similarly, you can use Ente's [desktop app](https://ente.io/download/desktop) + to export your data to a folder of your choice. The desktop app also supports + "continuous" exports, where it will automatically export new items in the + background without you needing to run any other cron jobs. See + [migration/export](/photos/migration/export/) for more details. + +## Does the exported data from Ente photos preserve the same folder and album structure as in the app? + +When you export your data for local backup, it will maintain the exact structure +how you have set up within Ente. The exported data will reflect the same photos +and album structure intact. diff --git a/docs/docs/photos/faq/general.md b/docs/docs/photos/faq/general.md new file mode 100644 index 000000000..c49b29979 --- /dev/null +++ b/docs/docs/photos/faq/general.md @@ -0,0 +1,102 @@ +--- +title: General FAQ +description: An assortment of frequently asked questions about Ente Photos +--- + +# General FAQ + +## How can I earn free storage? + +Use our [referral program](/photos/features/referral-program/). + +## What file formats does Ente support? + +Ente supports all files that have a mime type of `image/*` or `video/*` +regardless of their specific format. + +However, we only have limited support for RAW currently. We are working towards +adding full support, and you can watch this +[thread](https://github.com/ente-io/ente/discussions/625) for updates. + +If you find an issue with ente's ability to parse a certain file type, please +write to [support@ente.io](mailto:support@ente.io) with details of the +unsupported file format and we will do our best to help you out. + +## Is there a file size limit? + +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 +quality. + +But some of these formats cannot be streamed on the web browser and you will be +prompted to download them. + +## Why does Ente consume lesser storage than other providers? + +Most storage providers compute your storage quota in GigaBytes (GBs) by dividing +your total bytes uploaded by `1000 x 1000 x 1000`. + +Ente on the other hand, computes your storage quota in GibiBytes (GiBs) by +dividing your total bytes uploaded by `1024 x 1024 x 1024`. + +We decided to leave out the **i** from **GiBs** to reduce noise on our +interfaces. + +## Why should I trust Ente for long-term data-storage? + +Unlike large companies, we have a focused mission, to build a safe space where +you can easily archive your personal memories. + +This is the only thing we want to do, and with our pricing model, we can +profitably do it. + +We preserve your data end-to-end encrypted, and our open source apps have been +[externally audited](https://ente.io/blog/cryptography-audit/). + +Also, we have spent great deal of engineering effort into designing reliable +data replication and graceful disaster recovery plans. This is also done +transparently - we have documented the specifics of our replication and +reliability [here](https://ente.io/reliability). + +In short, we love what we do, we have no reasons to be distracted, and we are as +reliable as any one can be. + +If you would like to fund the development of this project, please consider +[subscribing](https://ente.io/download). + +## How do I pronounce ente? + +It's like cafe 😊. kaf-_ay_. en-_tay_. + +## Does Ente apply compression to uploaded photos? + +Ente does not apply compression to uploaded photos. The file size of your photos in Ente will be similar to the original file sizes you have. + +## Can I add photos from a shared album to albums that I created in Ente? + +Currently, Ente does not support adding photos from a shared album to your personal albums. If you want to include photos from a shared album in your own albums, you will need to ask the owner of the photos to add them to your album. + +## How do I ensure that the Ente desktop app stays up to date on my system? + +Ente desktop includes an auto-update feature, ensuring that whenever updates are deployed, the app will automatically download and install them. You don't need to manually update the software. + +## Can I sync a folder containing multiple subfolders, each representing an album? + +Yes, when you drag and drop the folder onto the desktop app, the app will detect the multiple folders and prompt you to choose whether you want to create a single album or separate albums for each folder. + +## What is the difference between **Magic** and **Content** search results on the desktop? + +**Magic** is where you can search for long queries. Like, "baby in red dress", or "dog playing at the beach". + +**Content** is where you can search for single-words. Like, "car" or "pizza". + +## 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." \ No newline at end of file diff --git a/docs/docs/photos/faq/hidden-and-archive.md b/docs/docs/photos/faq/hidden-and-archive.md new file mode 100644 index 000000000..bdfb6f1b0 --- /dev/null +++ b/docs/docs/photos/faq/hidden-and-archive.md @@ -0,0 +1,36 @@ +--- +title: Can I hide photos in ente? +description: Two related ways of hiding or archiving in Ente Photos +--- + +# Can I hide photos in ente? + +Yes, you can hide specific photos and videos in Ente using the "Hide" action. +Open the photo, expand the overflow menu and select Hide (the action with the +eye icon). + +Hidden items do not appear anywhere in Ente except within the special "Hidden" +category. Ente will ask for the device biometric (FaceID / TouchID) or passcode +to view the contents of the Hidden category. + +You can reach the Hidden category from the bottom of the albums screen. + +Keep in mind that hidden items will still show up in the "On device" albums +within Ente as long as they are present in your native gallery. But once you +remove them from your device, they'll stop showing up here. + +Hiding is currently only supported in the Ente mobile app, and items hidden from +the mobile app will not be visible in the web and desktop app. + +For more details, see [features/hide](/photos/features/hide). + +### Archive + +There is also a related feature called "Archive". While hidden items do not +appear anywhere, archived items do not appear in your timeline but can otherwise +be seen within the album and search results. + +This is useful when you're not trying to hide certain photos per se, but just do +not want some of them (say, some old screenshots) to clutter your home timeline. + +For more details, see [features/archive](/photos/features/archive). diff --git a/docs/docs/photos/faq/index.md b/docs/docs/photos/faq/index.md deleted file mode 100644 index c5a4e2cf9..000000000 --- a/docs/docs/photos/faq/index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: FAQ -description: Frequently asked questions about Ente Photos ---- - -# FAQ - -_Coming soon_. On this page we'll document some help items in a question and -answer format. diff --git a/docs/docs/photos/faq/security-and-privacy.md b/docs/docs/photos/faq/security-and-privacy.md new file mode 100644 index 000000000..b8479b140 --- /dev/null +++ b/docs/docs/photos/faq/security-and-privacy.md @@ -0,0 +1,82 @@ +--- +title: Security and privacy FAQ +description: + Frequently asked questions about security and privacy of Ente Photos +--- + +# Security and privacy + +## Can Ente see my photos and videos? + +No. + +Your files are encrypted with a key before they are uploaded to our servers. + +These keys can be accessed only with your password. + +Since only you know your password, only you can decrypt your files. + +To learn more about our encryption protocol, please read about our +[architecture](https://ente.io/architecture). + +## How is my data encrypted? + +We use [libsodium](https://libsodium.gitbook.io/doc/)'s implementations +`XChaCha20` and `XSalsa20` to encrypt your data, along with `Poly1305` MAC for +authentication. + +Please refer to the document on our [architecture](https://ente.io/architecture) +for more details. + +## Where is my data stored? + +Your data is replicated to multiple providers in different countries in the EU. + +Currently we have datacenters in the following locations: + +- Amsterdam, Netherlands +- Paris, France +- Frankfurt, Germany + +Much more details about our replication and reliability are documented +[here](https://ente.io/reliability). + +## What happens if I forget my password? + +You can reset your password with your recovery key. + +If you lose both your password and your recovery key, you will not be able to +decrypt your data. + +## Can I change my password? + +Yes. + +You can change your password from any of our apps. + +Thanks to our [architecture](https://ente.io/architecture), you can do so +without having to re-encrypt any of your files. + +The privacy of your account is a function of the strength of your password, +please choose a strong one. + +## Do you support 2FA? + +Yes. + +You can setup two-factor authentication from the settings screen of the mobile +app or from the side bar of our desktop app. + +## How does sharing work? + +The information required to decrypt an album is encrypted with the recipient's +public key such that only they can decrypt them. + +You can read more about this [here](https://ente.io/architecture#sharing). + +In case of sharable links, the key to decrypt the album is appended by the +client as a [fragment to the URL](https://en.wikipedia.org/wiki/URI_fragment), +and is never sent to our servers. + +Please note that only users on the paid plan are allowed to share albums. The +receiver just needs a free Ente account. diff --git a/docs/docs/photos/faq/subscription.md b/docs/docs/photos/faq/subscription.md new file mode 100644 index 000000000..ed4852151 --- /dev/null +++ b/docs/docs/photos/faq/subscription.md @@ -0,0 +1,162 @@ +--- +title: Subscription FAQ +description: Frequently asked questions about Ente Photos subscription and plans +--- + +# Subscription and plans + +See our [website](https://ente.io#pricing) for the list of supported plans and +pricing. + +## Does Ente have Family Plans? + +Yes we do! Please check out our announcement post +[here](https://ente.io/blog/family-plans). + +In brief, + +- Your family members can use storage space from your plan without paying + extra. + +- Ask them to sign up for Ente, and then just add them to your existing plan + using the "Manage family" option within your Subscription settings. + +- Each member gets their own private space, and cannot see each other's files + unless they're shared. + +- You can invite 5 family members. So including yourself, it will be 6 people + who can share a single subscription, paying only once. + +Note that family plans are meant as a way to share storage. For sharing photos, +you can create [shared albums and links](/photos/features/share). + +## Does Ente offer discounts to students? + +Yes we do! + +We believe that privacy should be made accessible to everyone. In this spirit, +we offer **30% off** our subscription plans to students. + +To apply for this discount, please verify your enrollment status in a school / +college / university by writing to [students@ente.io](mailto:students@ente.io) +from the email address assigned to you by your institute. + +In case you do not have access to such an email address, please send us proof +(such as your institute's identity card) that verifies your identity as a +student. + +Please note that these discounts are valid for a year, after which you may +reapply to reclaim the discount. + +## What payment methods does Ente support? + +On Web, Desktop and Android, Stripe helps us process payments from all major +prepaid and credit card providers. + +On iOS, we (have to) use the billing platforms provided by the app store. + +Apart from these, we also support PayPal and crypto currencies (more details +below). + +## Can I pay with PayPal? + +We support **annual** subscriptions over PayPal. + +Please drop an email to paypal@ente.io from your registered email address, +mentioning the [storage plan](https://ente.io#pricing) of your choice and we +will send you an invoice with a link to complete the payment. + +Once the payment is completed, your account will be upgraded to the chosen plan. + +## Does Ente accept crypto payments? + +We accept the following crypto currencies: + +- Bitcoin +- Ethereum +- Dogecoin + +To purchase a subscription with any of the above mentioned currencies, please +write to crypto@ente.io from your registered email address, citing the +[storage plan](https://ente.io#pricing) of your choice. + +In case you have any further questions or need support, please reach out to +[support@ente.io](mailto:support@ente.io), and we'll be happy to help! + +> Please note that Ente does not provide anonymity. What we provide is privacy, +> since your data is end-to-end encrypted. +> [Information](https://ente.io/privacy/#3-what-information-do-we-collect) we +> have about you might make your identity deducible. We are accepting crypto as +> a way to make Ente more accessible, not to provide anonymity. + +## Does Ente store my card details? + +Ente does not store any of your sensitive payment related information. + +We use [Stripe](https://stripe.com) to handle our card payments, and all of your +payment information is sent directly to Stripe's PCI DSS validated servers. + +Stripe has been audited by a PCI-certified auditor and is certified to +[PCI Service Provider Level 1](https://www.visa.com/splisting/searchGrsp.do?companyNameCriteria=stripe). +This is the most stringent level of certification available in the payments +industry. + +All of this said, if you would still like to pay without sharing your card +details, you can pay using PayPal. + +## What happens if I exceed my storage limit? + +Ente will stop backing up your files and you will receive an email alerting you +of the same. + +Your backed up files will remain accessible for as long as you have an active +subscription. + +## What happens when my subscription expires? + +30 days after your subscription expires, all of your uploaded data will be +cleared from our servers. + +You will receive an email prompting you to take out all of your backed up data +before this happens. + +## What happens when I upgrade my plan? + +Your new plan will go into effect immediately, and you only have to pay the +difference. We will adjust your remaining pro-rated balance on the old plan when +invoicing you for the new plan. + +For example, if you are half way through the year on the 100 GB yearly plan, and +upgrade to the 500 GB yearly plan, then + +- The new 500 GB yearly plan will go into effect immediately. + +- But we will reduce the charges for the first year by subtracting the + remaining half year balance of the 100 GB yearly plan that you'd already + paid. + +The same applies to monthly plans. + +## Is there an x GB plan? + +We have experimented quite a bit and have found it hard to design a single +structure that fits all needs. Some customers wish for many options, some even +wish to go to an extreme of dynamic per GB pricing. Other customers wish to keep +everything simple, some even wish for a single unlimited plan. + +To keep things fair, our plans don't increase linearly, and the tiers are such +that cover the most requested patterns. + +In addition, we also offer [family plans](/photos/features/family-plans) so that +you can gain more value out of a single subscription. + +## Is there a forever-free plan? + +Sorry, since we're building a business that does not involve monetization of +user data, we have to charge to remain sustainable. + +We do offer a generous free trial for you to experience the product. + +## Will I need to pay for Ente Auth after my Ente Photos free plan expires? + +No, you will not need to pay for Ente Auth after your Ente Photos free plan expires. Ente Auth is completely free to use, and the expiration of your Ente Photos free plan will not impact your ability to access or use Ente Auth. diff --git a/docs/docs/photos/features/background.md b/docs/docs/photos/features/background.md new file mode 100644 index 000000000..f2f437453 --- /dev/null +++ b/docs/docs/photos/features/background.md @@ -0,0 +1,57 @@ +--- +title: Background sync +description: Ente Photos supports automatic background sync and backup +--- + +# Background sync + +Ente Photos supports seamless background sync so that you don't need to open the +app to backup your photos. It will sync in the background and automatically +backup the albums that you have selected for syncing. + +Day to day sync will work automatically. However, there are some platform +specific considerations that apply, more on these below: + +### iOS + +On iOS, if you have a very large number of photos and videos, then you might +need to keep Ente running in the foreground for the first backup to happen +(since we get only a limited amount of background execution time). To help with +this, under "Settings > Backup" there is an option to disable the automatic +device screen lock. But once your initial backup has completed, subsequent +backups will work fine in the background and don't need disabling the screen +lock. + +On iOS, Ente will not backup videos in the background (since videos are usually +much larger and need more time to upload than what we get). However, they will +get backed up the next time the Ente app is opened. + +Note that the Ente app will not be able to backup in the background if you force +kill the app. + +> If you're curious, the way this works is, our servers "tickle" your device +> every once in a while by sending a silent push notification, which wakes up +> our app and gives it 30 seconds to execute a background sync. However, if you +> have killed the app from recents, iOS will not deliver the push to the app, +> breaking the background sync. + +### Android + +On some Android versions, newly downloaded apps activate a mode called "Optimize +battery usage" which prevents them from running in the background. So you will +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. + +### Desktop + +In addition to our mobile apps, the background sync also works on our desktop +app, though the [way that works](watch-folders) is a bit different. + +--- + +## Troubleshooting + +- On iOS, make sure that you're not killing the Ente app. +- On Android, make sure that "Optimize battery usage" is not turned on in + system settings for the Ente app. diff --git a/docs/docs/photos/features/backup.md b/docs/docs/photos/features/backup.md new file mode 100644 index 000000000..a53725200 --- /dev/null +++ b/docs/docs/photos/features/backup.md @@ -0,0 +1,22 @@ +--- +title: Backup +description: Details about how backup works in Ente Photos +--- + +# Backup + +Ente will automatically backup any albums in your native photos app that you +select for backup. + +Ente will run in the background, and any new photos added to these albums (or +any photos in these albums that were modified) will be automatically synced to +ente. + +You can choose which albums should be backed up when you sign up for Ente. If +you change your mind later, or if you create a new album in your native photos +app that you also want to backup, please use "Settings > Backup > Backed up +folders" to modify your choices. + +If a file is deleted on your native photos app, it will still show up in Ente. +This is because on both iOS and Android, apps are not allowed to automatically +delete user's photos without a manual confirmation. diff --git a/docs/docs/photos/features/collaborate.md b/docs/docs/photos/features/collaborate.md new file mode 100644 index 000000000..32fb71fc1 --- /dev/null +++ b/docs/docs/photos/features/collaborate.md @@ -0,0 +1,63 @@ +--- +title: Collaboration +description: Collaborate with other people using shared albums and public links +--- + +# Collaborate + +Ente allows you to collaborate with people in 2 ways: + +- Collaborative albums + +- Collaborative links + +## Collaborative albums + +Collaborative albums allow multiple Ente users to add photos to the same shared +album. Storage is only counted once, irrespective of the number of collaborators +and viewers. + +- The owner of the album is the person who created it. + +- The owner can add collaborators and viewers by their email. The owner can + also change permissions of participants at any time, and remove them. + +- Collaborators can add photos (and videos) to the shared album. + +- The storage of the photo is counted towards the owner of the photo - the + person who uploaded it. Since the uploader usually has the photo in their + account anyway, effectively this means that the photo can be added to a + collaborative album without paying anything extra. + +- The owner of the photo can remove it from the album (or delete it). + +- The owner of the album can remove all photos from the album (they can only + delete the photos they own). + +- When a collaborator is removed from a shared album (or when they leave the + album), any photos they'd uploaded will also be removed. + +Currently collaborative albums can only be used from the mobile app. A +collaborator will see them in view only mode in the web and desktop apps; we're +actively working on adding support for them on web and desktop too. + +## Collaborative links + +Collaborative links allow you to collaborate with people who might not have an +Ente account or the Ente apps. + +- You can create a public link, and anyone with access to the link will be + able to view the shared photos using just their web browser (no login + required). + +- You can enable the "Allow adding photos" option on a public link to allow + people to also add photos the same way (from their web browser, no login + required). + +Such collaborative links are also sometimes called "collect links", since they +allow you to collect photos from people without them needing Ente accounts. A +common use case for this is collecting event and trip photos from a big circle +of people. + +The storage for the photos added to a collaborative link are counted towards the +album owner. The owner can also remove these photos at any time. diff --git a/docs/docs/photos/features/deduplicate.md b/docs/docs/photos/features/deduplicate.md new file mode 100644 index 000000000..02db7b5cb --- /dev/null +++ b/docs/docs/photos/features/deduplicate.md @@ -0,0 +1,59 @@ +--- +title: Deduplicate +description: Removing duplicates photos using Ente Photos +--- + +# Deduplicate + +Ente performs two different duplicate detections: one during uploads, and one +that can be manually run afterwards to remove duplicates across albums. + +## During uploads + +Ente will automatically deduplicate and ignore duplicate files during uploads. + +When uploading, Ente will ignore exact duplicate files. This allows you to +resume interrupted uploads, or drag and drop the same folder, or reinstall the +app, and expect Ente to automatically skip duplicates and only add new files. + +The duplicate detection works slightly different on each platform, to cater to +the platform's nuances. + +#### Mobile + +- On iOS, a hash will be used to detect exact duplicates. If the duplicate is + being uploaded to an album where a photo with the same hash already exists, + then the duplicate will be skipped. If it is being uploaded to a different + album, then a symlink will be created (so no actual data will need to be + uploaded, just a symlink will be created to the existing file). + +- On Android also, a hash check is used. But unlike iOS, the native Android + filesystem behaviour is to keep physical copies if the same photo is in + different albums. So Ente does the same: duplicates to same album will be + skipped, duplicates when going to separate albums will create copies. + +#### Web and desktop + +On laptops (i.e. when using the Ente web or desktop app), in addition to a hash +check, the file name is also used. The assumption is that the user wishes to +keep two copies if they have the same file but with different names. + +Thus a file will be considered a duplicate and skipped during upload if a file +with the same name and hash already exists in the album. + +And if you're trying to upload it to a different album (i.e. the same file with +the same name already exists in a different album), then a symlink to the +existing file will be created. This is similar to what happens when you do "Add +to album", and the actual files are not re-uploaded. + +## Manual deduplication + +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. + +## Adding to Ente album creates symlinks + +Note that once a file in is Ente, adding it to another Ente album will create a +symlink, so that you can add it to as many albums as you wish but storage will +only be counted once. diff --git a/docs/docs/photos/features/family-plans.md b/docs/docs/photos/features/family-plans.md index 8405c7dcf..ee38c5ae6 100644 --- a/docs/docs/photos/features/family-plans.md +++ b/docs/docs/photos/features/family-plans.md @@ -22,3 +22,13 @@ In brief, - You can invite 5 family members. So including yourself, it will be 6 people who can share a single subscription, paying only once. + +## FAQ + +- **Can you assign a storage quota for each individual member in the family + plan?** + + Unfortunately, at this moment, assigning a storage quota for each individual + member in the family plan is not supported. For updates on this feature + request, please follow + [this thread](https://github.com/ente-io/ente/discussions/857). diff --git a/docs/docs/photos/features/free-up-space/free-up-space.png b/docs/docs/photos/features/free-up-space/free-up-space.png new file mode 100644 index 000000000..60fdf85f1 Binary files /dev/null and b/docs/docs/photos/features/free-up-space/free-up-space.png differ diff --git a/docs/docs/photos/features/free-up-space/index.md b/docs/docs/photos/features/free-up-space/index.md new file mode 100644 index 000000000..77064d080 --- /dev/null +++ b/docs/docs/photos/features/free-up-space/index.md @@ -0,0 +1,18 @@ +--- +title: Free up space +description: Freeing up your phone's storage space when using Ente Photos +--- + +# Free up your phone's storage space + +Within the app's settings page, you have an option to free up space by deleting +all backed up photos and videos from your phone's internal storage. + +
+ +![Free up space screen](free-up-space.png){width=400px} + +
+ +> Note: You might have to clear the device's trash to realize this cleared +> space. diff --git a/docs/docs/photos/features/public-links.md b/docs/docs/photos/features/public-link.md similarity index 96% rename from docs/docs/photos/features/public-links.md rename to docs/docs/photos/features/public-link.md index 7f079b82f..e20dfb481 100644 --- a/docs/docs/photos/features/public-links.md +++ b/docs/docs/photos/features/public-link.md @@ -1,11 +1,11 @@ --- -title: Public links +title: Public link description: Share photos with your friends and family without them needing to install Ente Photos --- -# Public Links +# Public link Ente lets you share your photos via links, that can be accessed by anyone, without an app or account. diff --git a/docs/docs/photos/features/quick-link.md b/docs/docs/photos/features/quick-link.md index e32863f0f..e1b8bd201 100644 --- a/docs/docs/photos/features/quick-link.md +++ b/docs/docs/photos/features/quick-link.md @@ -1,9 +1,9 @@ --- -title: Quick links +title: Quick link description: Share photos with your friends and family without creating albums --- -# Quick Links +# Quick link Quick links allows you to select single or multiple photos and create a link that you can then share. You don't need to create an album first. @@ -18,5 +18,5 @@ that you can then share. You don't need to create an album first. - Removing a link will not delete the photos that are present in that link. -- Similar to [public-links](./public-links), you can set link expiry, +- Similar to a [public-link](./public-link), you can set link expiry, passwords or device limits. diff --git a/docs/docs/photos/features/referral-program/free-storage.png b/docs/docs/photos/features/referral-program/free-storage.png new file mode 100644 index 000000000..90d3ab860 Binary files /dev/null and b/docs/docs/photos/features/referral-program/free-storage.png differ diff --git a/docs/docs/photos/features/referral-program/index.md b/docs/docs/photos/features/referral-program/index.md new file mode 100644 index 000000000..f61bea6b9 --- /dev/null +++ b/docs/docs/photos/features/referral-program/index.md @@ -0,0 +1,65 @@ +--- +title: Referral program +description: Earn free storage by referring Ente Photos to your friends +--- + +# Referral program + +You can refer your friends to earn free storage on Ente. + +For each friend you refer, who upgrades to a paid plan, we will credit **10 GB** +of free storage. The referred customer will also receive an additional **10 GB** +with their paid subscription. + +That is, if you refer a friend, once your friend upgrades to a paid plan, both +you and your friend receive an additional 10 GB of storage. + +You can find your referral code under _Settings → General → Referrals_. + +
+ +![Claim free storage screen](free-storage.png){width=400px} + +
+ +### How much storage can I earn? + +The amount of free storage you can earn is capped to your current plan. This +means, you can at max double your storage. For example, if you're on a +100 GB plan, you can earn another 100 GB (by referring 10 friends), taking your +total available storage to 200 GB. + +You can keep track of your earned storage and referral details on _Claim free +storage_ screen. + +If you refer more paid customers than is allowed by your current plan, the extra +storage earned will be reserved and will become usable once you upgrade your +plan. + +### For how long do I have access to this storage? + +Earned storage will be accessible as long as your subscription is active, +provided there has been no abuse. + +In case our systems detect abuse, we may notify you and take back credited +storage. Low quality referrals (who don't renew their plans) or creation of fake +accounts, etc. could result in this. + +### How can my friends apply my referral code? + +Referral codes can be applied within _Settings → General → Referrals → Apply +Code_. + +
+ +![Apply referral code screen](referral-code-application.png){width=400px} + +
+ +Please note that referral codes should be applied within one month of account +creation to claim free storage. + +--- + +More questions? Drop a mail to [referrals@ente.io](mailto:referrals@ente.io), +and we'll get back to you! diff --git a/docs/docs/photos/features/referral-program/referral-code-application.png b/docs/docs/photos/features/referral-program/referral-code-application.png new file mode 100644 index 000000000..27686e904 Binary files /dev/null and b/docs/docs/photos/features/referral-program/referral-code-application.png differ diff --git a/docs/docs/photos/features/referrals.md b/docs/docs/photos/features/referrals.md deleted file mode 100644 index 085a0b19b..000000000 --- a/docs/docs/photos/features/referrals.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Referral plan -description: - Earn and expand your storage by referring Ente Photos to your friends and - family ---- - -# Referral plan - -_Earn and Expand Your Storage_ - -Did you know you can boost your storage on Ente simply by referring your -friends? Our referral program lets you earn 10 GB of free storage for each -friend who upgrades to a paid plan, and your referred friends receive an -additional 10 GB with their subscription. - -## How to Refer a friend? - -On the Home Page: - -- Click on the hamburger menu in the top left corner -- Open the sidebar -- Tap on _General_ -- Select _Referrals_ -- Share the code with your friend or family - -Note: - -- Once your friend upgrades to a paid plan, both you and your friend receive - an additional 10 GB of storage. -- You can keep track of your earned storage and referral details on _Claim - free storage_ screen. -- If you refer more friends than your plan allows, the extra storage earned - will be reserved until you upgrade your plan. -- Earned storage remains accessible as long as your subscription is active. - -## How to apply referral code given by a friend? - -On the Home Page: - -- Click on the hamburger menu inthe top left corner -- Tap on _General_ from the options -- Select _Referrals_ from the menu -- Find and tap on _Apply Code_ -- Enter the referral code provided by your friend. - -Please note that referral codes should be applied within one month of account -creation to claim the free storage. diff --git a/docs/docs/photos/features/share.md b/docs/docs/photos/features/share.md new file mode 100644 index 000000000..a1b9be376 --- /dev/null +++ b/docs/docs/photos/features/share.md @@ -0,0 +1,75 @@ +--- +title: Share +description: Securely share photos and videos stored in Ente Photos +--- + +# Sharing + +Ente supports end-to-end encrypted sharing of your photos and videos. + +This allows you to share your photos and videos with only the people you want, +without them being visible to anybody else. The files remain encrypted at all +times, and only the people you have shared with get the decryption keys. + +- If the person you want to share with is already on Ente, you can share an + album with them by entering their email address. + +- If they are not already on Ente, you can send them an invite and then share + with them after they've signed up. + +- Alternatively, you can create public links to share albums with people who + are not on Ente. + +With public links, the files are still end-to-end encrypted, so the sharing is +still secure. Note that the decryption keys are part of the public link so keep +in mind that anybody with the link will be able to share it with others. + +Both shared albums and public links allow [collaboration](collaborate). + +## Links + +You can create links to your albums by opening an album and clicking on the +Share icon. They are publicly accessible by anyone who you share the link with. +They don't need an app or account. + +These links can be password protected, or set to expire after a while. + +You can read more about the features supported by Links +[here](https://ente.io/blog/powerful-links/). + +## Albums + +If your loved ones are already on Ente, you can share an album with their +registered email address. + +If they are your partner, you can share your `Camera` folder on Android, or +`Recents` on iOS. Whenever you click new photos, they will automatically be +accessible on your partner's device. + +## Collaboration + +You can allow other Ente users to add photos to your album. This is a great way +for you to build an album together with someone. You can control access to the +same album - someone can be added as a `Collaborator`, while someone else as a +`Viewer`. + +If you wish to collect photos from folks who are not Ente, you can do so with +our Links. Simply tick the box that says "Allow uploads", and anyone who has +access to the link will be able to add photos to your album. + +## Technical details + +More details, including technical aspect about how the sharing features were +implemented, are in various blog posts announcing these features. + +- [Collaborative albums](https://ente.io/blog/collaborative-albums) + +- [Collect photos from people not on ente](https://ente.io/blog/collect-photos) + +- [Shareable links for albums](https://ente.io/blog/shareable-links), + [and their underlying technical implementation](https://ente.io/blog/building-shareable-links). + Since then, we have also added the ability to password protect public links, + and configure a duration after which the link will automatically expire. + +We are now working on the other requested features around sharing, including +comments and reactions. diff --git a/docs/docs/photos/features/sharing.md b/docs/docs/photos/features/sharing.md deleted file mode 100644 index 9f32830ec..000000000 --- a/docs/docs/photos/features/sharing.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Sharing -description: - Ente allows you to share albums and collaborate with your loved ones ---- - -# Sharing - -It is easy to share your albums on Ente, end-to-end encrypted. - -## Links - -You can create links to your albums by opening an album and clicking on the -Share icon. They are publicly accessible by anyone who you share the link with. -They don't need an app or account. - -These links can be password protected, or set to expire after a while. - -You can read more about the features supported by Links -[here](https://ente.io/blog/powerful-links/). - -## Albums - -If your loved ones are already on Ente, you can share an album with their -registered email address. - -If they are your partner, you can share your `Camera` folder on Android, or -`Recents` on iOS. Whenever you click new photos, they will automatically be -accessible on your partner's device. - -## Collaboration - -You can allow other Ente users to add photos to your album. This is a great way -for you to build an album together with someone. You can control access to the -same album - someone can be added as a `Collaborator`, while someone else as a -`Viewer`. - -If you wish to collect photos from folks who are not Ente, you can do so with -our Links. Simply tick the box that says "Allow uploads", and anyone who has -access to the link will be able to add photos to your album. -[Read more](https://ente.io/blog/collect-photos/) diff --git a/docs/docs/photos/features/watch-folder.md b/docs/docs/photos/features/watch-folder.md deleted file mode 100644 index 966f35be5..000000000 --- a/docs/docs/photos/features/watch-folder.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Watch folder -description: Automatic syncing of certain folders in the Ente Photos desktop app ---- - -# Watch folder - -_Automatic syncing_ - -The Ente desktop app allows you to "watch" a folder on your computer for any -changes, creating a one-way sync from your device to the Ente cloud. This is -intended to automate your photo management and backup. - -## How to add Watch folders? - -- Click on the hamburger menu in the top left corner -- Open the sidebar -- Select _Watch Folders_ -- Choose _Add Watch Folders_ -- Pick the folder from your system that you want to add as a watched folder - -## How to remove Watch folders? - -- Click on the hamburger menu in the top left corner -- Open the sidebar -- Select _Watch Folders_ -- Click on the three dots menu next to the folders on the right side -- Choose _Stop Watching_ from the menu - -# Tips: - -- You will get an option to choose whether to sync nested folders to a single - album or separate albums. - -- The app continuously monitors changes in the watched folder, such as the - addition or removal of files. diff --git a/docs/docs/photos/features/watch-folders.md b/docs/docs/photos/features/watch-folders.md new file mode 100644 index 000000000..6a7d50a15 --- /dev/null +++ b/docs/docs/photos/features/watch-folders.md @@ -0,0 +1,68 @@ +--- +title: Watch folder +description: + Automatic syncing of selected folders using the Ente Photos desktop app +--- + +# Watch folders + +The Ente desktop app allows you to "watch" a folder on your computer for any +changes, creating a one-way background sync from folders on your computer to +Ente albums. This is intended to automate your photo management and backup. + +By using the "Watch folders" option in the sidebar, you can tell the desktop app +which are the folders that you want to watch for changes. The app will then +automatically upload new files added to these folders to the corresponding ente +album (it will also upload them initially). And if a file is deleted locally, +then the corresponding Ente file will also be automatically moved to +uncategorized. + +Paired with the option to run Ente automatically when your computer starts, this +allows you to automate backups to ente's cloud. + +### Steps + +1. Press the **Watch folders** button in the sidebar. This will open up a dialog + where you can add and remove watched folders. + +2. To start watching a folder, press the **Add folder** button and select the + folder on your laptop that you want to watch for any changes. You can also + drag and drop the folder here. + +3. If the folder has nesting, you will see two options - **A single album** and + **Separate albums**. + + - **Single album** will create a new Ente album with the same name as the + folder's name, and will then sync all the changes in the folder (and any + nested folders) to this single album. + + - **Separate albums** will create separate albums for each nested folder of + the selected folder, and will then sync the changes in each nested folder + separately. + + - For example, suppose you have a folder name `Photos` on your computer, and + inside that folder you have two nested folders named `New Year` and + `Summer`. In the single album mode, the app will create an Ente album + named "Photos" and put all the files from both `New Year` and `Summer` + there. In the separate album mode, the app will create two Ente albums, + "New Year" and "Summer", each only containing the respective files. + + - In separate album mode, only nested folders that have at least one file + will result in the creation of a new album – empty folders (or folders + that only contain other folders) will be ignored. + +4. After choosing any of the above options, the folder will be initially synced + to ente's cloud and monitored for any changes. You can now close the dialog + and the sync will continue in background. + +5. When the app is syncing in the background it'll show a small progress status + in the bottom right. You can expand it to see more details if needed. + +6. You can stop watching any folder by clicking on the three dots next to the + watch folder entry, and then selecting **Stop watching**. + +> Note: In case you start a new upload while an existing sync is in progress, +> the sync will be paused then and resumed when your upload is done. + +Some more details about the feature are in our +[blog post](http://ente.io/blog/watch-folders) announcing it. diff --git a/docs/docs/photos/index.md b/docs/docs/photos/index.md index 2b5d2e752..0249c0d3a 100644 --- a/docs/docs/photos/index.md +++ b/docs/docs/photos/index.md @@ -10,9 +10,10 @@ Photos. You can use it to safely and securely store your photos on the cloud. While security and privacy form the bedrock of Ente Photos, it is not at the cost of usability. The user interface is simple, and we are continuously working -to make it even simpler. The goal is a product that can be used by people with -all sorts of technical ability and background. +to make it even simpler. -These help docs are divided into three sections: Features, FAQ and -Troubleshooting. Choose the relevant page from the sidebar menu, or use the -search at the top. +The goal is a product that can be used by people with all sorts of technical +ability and background. + +These help docs are divided into four sections. Choose the relevant page from +the sidebar menu, or use the search at the top. diff --git a/docs/docs/photos/migration/export/continuous-sync.webp b/docs/docs/photos/migration/export/continuous-sync.webp new file mode 100644 index 000000000..f5b1346eb Binary files /dev/null and b/docs/docs/photos/migration/export/continuous-sync.webp differ diff --git a/docs/docs/photos/migration/export/export-1.png b/docs/docs/photos/migration/export/export-1.png new file mode 100644 index 000000000..49f03b28c Binary files /dev/null and b/docs/docs/photos/migration/export/export-1.png differ diff --git a/docs/docs/photos/migration/export/export-2.png b/docs/docs/photos/migration/export/export-2.png new file mode 100644 index 000000000..f181a2422 Binary files /dev/null and b/docs/docs/photos/migration/export/export-2.png differ diff --git a/docs/docs/photos/migration/export/export-3.png b/docs/docs/photos/migration/export/export-3.png new file mode 100644 index 000000000..3df2a5660 Binary files /dev/null and b/docs/docs/photos/migration/export/export-3.png differ diff --git a/docs/docs/photos/migration/export/export-4.png b/docs/docs/photos/migration/export/export-4.png new file mode 100644 index 000000000..e8abcf0f2 Binary files /dev/null and b/docs/docs/photos/migration/export/export-4.png differ diff --git a/docs/docs/photos/migration/export/export-5.png b/docs/docs/photos/migration/export/export-5.png new file mode 100644 index 000000000..aa939e566 Binary files /dev/null and b/docs/docs/photos/migration/export/export-5.png differ diff --git a/docs/docs/photos/migration/export/index.md b/docs/docs/photos/migration/export/index.md new file mode 100644 index 000000000..c1bac2ae9 --- /dev/null +++ b/docs/docs/photos/migration/export/index.md @@ -0,0 +1,70 @@ +--- +title: Exporting your data from Ente Photos +description: Guide for exporting your photos out from Ente Photos +--- + +# Exporting your data out of Ente Photos + +Please follow the following simple steps to keep a local copy of the photos and +videos you have uploaded to Ente. + +1. Sign in to [our desktop app](https://ente.io/download/desktop), if you + haven't done so already. + + ![Ente - Sign in to export data](sign-in.png) + +2. Open the side bar, and select the option to **Export Data**. + + ![Ente - Export data](export-1.png) + +3. Choose the destination folder by clicking on three dots icon. + +
+ +![Ente - Select destination folder and start](export-2.png){width=400px} + +
+ +4. Select the folder and then click on **Start** + +
+ +![Ente - Export in progress](export-3.png){width=400px} + +
+ +5. Wait for the export to complete. + +
+ +![Ente - Rexport](export-4.png){width=400px} + +
+ +6. In case your download gets interrupted, Ente will resume from where it left + off. Simply select **Export Data** again and click on **Resync**. + +
+ +![Ente - Rexport](export-5.png){width=400px} + +
+ + +### Sync continuously + +You can switch on the toggle to **Sync continuously** to eliminate manual +exports each time new photos are added to Ente. This feature automatically +detects new files and runs exports accordingly. It also ensures that exported +data reflects the latest album states with new files, moves, and deletions. + +![Ente - Continuous sync](continuous-sync.webp) + +--- + +If you run into any issues during your data export, please reach out to +[support@ente.io](mailto:support@ente.io) and we will be happy to help you! + +Note that we also provide a [CLI +tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your data. +Please find more details [here](/photos/faq/export). diff --git a/docs/docs/photos/migration/export/sign-in.png b/docs/docs/photos/migration/export/sign-in.png new file mode 100644 index 000000000..4a9fa4a23 Binary files /dev/null and b/docs/docs/photos/migration/export/sign-in.png differ diff --git a/docs/docs/photos/migration/from-amazon-photos.md b/docs/docs/photos/migration/from-amazon-photos.md new file mode 100644 index 000000000..185234523 --- /dev/null +++ b/docs/docs/photos/migration/from-amazon-photos.md @@ -0,0 +1,24 @@ +--- +title: Import from Amazon Photos +description: Migrating your existing photos from Amazon Photos to Ente Photos +--- + +# Import from Amazon Photos + +Amazon Photos does not provide a way to export all of your photos and videos, or +even albums with a single click. + +According to their +[help desk article](https://www.amazon.com/gp/help/customer/display.html?nodeId=GVCELKY5JW77VE7W), +you have to select and download photos individually. + +Once you've done that, simply drag and drop this folder into +[our desktop app](https://ente.io/download/desktop), and Ente will take care of +the rest. + +> Note: In case your uploads get interrupted, just drag and drop the folder into +> the same album again, and we will ignore already backed up files and upload +> just the rest. + +If you run into any issues during this migration, please reach out to +[support@ente.io](mailto:support@ente.io) and we will be happy to help you! diff --git a/docs/docs/photos/migration/from-apple-photos/export.png b/docs/docs/photos/migration/from-apple-photos/export.png new file mode 100644 index 000000000..aa0ba149a Binary files /dev/null and b/docs/docs/photos/migration/from-apple-photos/export.png differ diff --git a/docs/docs/photos/migration/from-apple-photos/index.md b/docs/docs/photos/migration/from-apple-photos/index.md new file mode 100644 index 000000000..80735bbd7 --- /dev/null +++ b/docs/docs/photos/migration/from-apple-photos/index.md @@ -0,0 +1,34 @@ +--- +title: Import from Apple Photos +description: Migrating your existing photos from Apple Photos to Ente Photos +--- + +# Import from Apple Photos + +The Apple Photos app provides an easy way download all your data. + +Select the files you want to export (`Command + A` to select them all), and +click on `File` > `Export` > `Export Unmodified Originals`. + +![Apple Photos - Export](export.png) + +In the dialog that pops up, select File Name as `Sequential` and provide any +prefix you'd like. This is to make sure that we combine the photo and video +portions of your Live Photos correctly. + +![Apple Photos - Sequential file names](sequential.png) + +Finally, choose an export directory and confirm by clicking `Export Originals`. +You will receive a notification from the app once your export is complete. + +Now simply drag and drop the downloaded folders into +[our desktop app](https://ente.io/download/desktop) and grab a cup of coffee (or +a good night's sleep, depending on the size of your library) while we handle the +rest. + +> Note: In case your uploads get interrupted, just drag and drop the folders +> into the same albums again, and we will ignore already backed up files and +> upload just the rest. + +If you run into any issues during this migration, please reach out to +[support@ente.io](mailto:support@ente.io) and we will be happy to help you! diff --git a/docs/docs/photos/migration/from-apple-photos/sequential.png b/docs/docs/photos/migration/from-apple-photos/sequential.png new file mode 100644 index 000000000..92f29d45c Binary files /dev/null and b/docs/docs/photos/migration/from-apple-photos/sequential.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-photos-1.png b/docs/docs/photos/migration/from-google-photos/google-photos-1.png new file mode 100644 index 000000000..932c772b2 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-photos-1.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-photos-2.png b/docs/docs/photos/migration/from-google-photos/google-photos-2.png new file mode 100644 index 000000000..a72fd6220 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-photos-2.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-photos-3.png b/docs/docs/photos/migration/from-google-photos/google-photos-3.png new file mode 100644 index 000000000..e600ce8e9 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-photos-3.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-photos-4.png b/docs/docs/photos/migration/from-google-photos/google-photos-4.png new file mode 100644 index 000000000..f3cf57007 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-photos-4.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-photos-5.png b/docs/docs/photos/migration/from-google-photos/google-photos-5.png new file mode 100644 index 000000000..48bf259d7 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-photos-5.png differ diff --git a/docs/docs/photos/migration/from-google-photos/google-takeout.png b/docs/docs/photos/migration/from-google-photos/google-takeout.png new file mode 100644 index 000000000..4cc580b39 Binary files /dev/null and b/docs/docs/photos/migration/from-google-photos/google-takeout.png differ diff --git a/docs/docs/photos/migration/from-google-photos/index.md b/docs/docs/photos/migration/from-google-photos/index.md new file mode 100644 index 000000000..577a3283e --- /dev/null +++ b/docs/docs/photos/migration/from-google-photos/index.md @@ -0,0 +1,62 @@ +--- +title: Import from Google Photos +description: Migrating your existing photos from Google Photos to Ente Photos +--- + +# Import from Google Photos + +Follow the following steps to recover your data from Google Photos and preserve +it with Ente. + +### Steps + +1. Open [takeout.google.com](https://takeout.google.com). + +2. Click on "Deselect All" (since by default all Google services are selected). + + ![Google Takeout - Create a new export](google-photos-1.png) + +3. Scroll down to find Google Photos in the list and select it by clicking the + check box next to it. + + ![Google Takeout - Select Google Photos](google-photos-2.png) + +4. Click on the button that says "All photo albums included". + +5. Select the albums you want to export. + + ![Google Takeout - Select albums](google-photos-3.png) + +6. Scroll down and click on "Next Step". + + ![Google Takeout - Next](google-photos-4.png) + +7. Select "Frequency" and "File size" depending on the amount of storage on your + system and click on "Create export". Make sure you select ZIP as the format. + + ![Google Takeout - Frequency and file size](google-photos-5.png) + +8. Wait for Google to send you your data. + +9. Open [our desktop app](https://ente.io/download/desktop), click on "Upload", + select "Google takeout" and pick the ZIP file you just downloaded. + + ![Importing Google Takeout into Ente](google-takeout.png){width=400px} + +10. Wait for the uploads to complete as Ente parses the metadata generated by + Google, and preserves them along with the corresponding files, end-to-end + encrypted! + +--- + +In case your uploads get interrupted, just drag and drop the file again and we +will ignore already backed up files and upload just the rest. + +If you run into any issues during this migration, please reach out to +[support@ente.io](mailto:support@ente.io) and we will be happy to help you! + +> Note: When importing a Google takeout, Ente will parse the metadata in the +> JSON files and stich them together with corresponding files. However, one case +> this will not work is when Google has split the export into multiple parts, +> and did not put the JSON file associated with an image in the same exported +> zip. diff --git a/docs/docs/photos/migration/from-local-hard-disk.md b/docs/docs/photos/migration/from-local-hard-disk.md new file mode 100644 index 000000000..91971ba19 --- /dev/null +++ b/docs/docs/photos/migration/from-local-hard-disk.md @@ -0,0 +1,19 @@ +--- +title: Import from local hard disk +description: + Migrating to Ente Photos by importing data from your local hard disk +--- + +# Import photos from your local hard disk + +Simply drag and drop the folders you want to preserve into +[our desktop app](https://ente.io/download/desktop) and grab a cup of coffee (or +a good night's sleep depending on the size of your library), while we will take +care of the rest. + +> Note: in case your uploads get interrupted, just drag and drop the folders +> into the same albums again, and we will ignore already backed up files and +> upload just the rest. + +If you run into any issues during uploads, please reach out to +[support@ente.io](mailto:support@ente.io) and we will be happy to help you! diff --git a/docs/docs/photos/migration/index.md b/docs/docs/photos/migration/index.md new file mode 100644 index 000000000..4bc71e871 --- /dev/null +++ b/docs/docs/photos/migration/index.md @@ -0,0 +1,14 @@ +--- +title: Migrating +description: + Guides for migrating your existing photos into Ente Photos and exporting + your data out of Ente Photos +--- + +# Migrating to/from Ente Photos + +- [Import from Google Photos](from-google-photos/) +- [Import from Apple Photos](from-apple-photos/) +- [Import from Amazon Photos](from-amazon-photos) +- [Import from local hard disk](from-local-hard-disk) +- [Export out of Ente Photos](export/) diff --git a/docs/docs/photos/troubleshooting/sharing-logs.md b/docs/docs/photos/troubleshooting/sharing-logs.md index 65090a76a..3015a691f 100644 --- a/docs/docs/photos/troubleshooting/sharing-logs.md +++ b/docs/docs/photos/troubleshooting/sharing-logs.md @@ -15,12 +15,31 @@ the logs just make the process a bit faster and easier. ### Mobile -Steps for mobile. Still a placeholder. +To **_Report a bug_** on your mobile device, follow these steps: + +- Tap on the three horizontal lines to access the settings. +- Tap on **"Support"** from the settings. +- Select for the option to **"Report a Bug"**. +- Tap on **"Report a bug"** . ### Desktop -Placeholder +To **_Report a bug_** on the desktop app, follow these steps: + +- Click on the three horizontal lines located in the top left corner of the + screen to access the settings. +- Click on **"Debug logs"** from the settings. +- Click on **Download logs**. +- Then Click on **"Support"**. +- Attach the downloaded logs in the email and describe the issue. ### Web -Placeholder +To **_Report a bug_** on the web, follow these steps: + +- Click on the three horizontal lines located in the top left corner of the + screen to access the settings. +- Click on **"Debug Logs"** +- Click on **Download logs** +- Click on **"Support"** from the settings. +- Attach the downloaded logs in the email and describe the issue. diff --git a/docs/docs/self-hosting/faq/index.md b/docs/docs/self-hosting/faq/index.md new file mode 100644 index 000000000..622e74a24 --- /dev/null +++ b/docs/docs/self-hosting/faq/index.md @@ -0,0 +1,33 @@ +--- +title: FAQ - Self hosting +description: Frequently asked questions about self hosting Ente +--- + +# Frequently Asked Questions + +### Do Ente Photos and Ente Auth share the same backend? + +Yes. The apps share the same backend, the same database and the same object +storage namespace. The same user account works for both of them. + +### Can I just self host Ente Auth? + +Yes, if you wish, you can self-host the server and use it only for the 2FA auth +app. The starter Docker compose will work fine for either Photos or Auth (or +both!) + +### Can I use the server with _X_ as the object storage? + +Yes. As long as whatever X you're using provides an S3 compatible API, you can +use it as the underlying object storage. For example, the starter self-hosting +Docker compose file we offer uses MinIO, and on our production deployments we +use Backblaze/Wasabi/Scaleway. But that's not the full list - as long as the +service you intend to use has a S3 compatible API, it can be used. + +### How do I increase storage space for users on my self hosted instance? + +See the [guide for administering your server](/self-hosting/guides/admin). In +particular, you can use the `ente admin update-subscription` CLI command to +increase the +[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) +of accounts on your instance. diff --git a/docs/docs/self-hosting/faq/otp.md b/docs/docs/self-hosting/faq/otp.md index a224d1f66..1ffe860e4 100644 --- a/docs/docs/self-hosting/faq/otp.md +++ b/docs/docs/self-hosting/faq/otp.md @@ -1,6 +1,6 @@ --- title: Verification code -description: Getting the OTP for a self host Ente +description: Getting the OTP for a self hosted Ente --- # Verification code diff --git a/docs/docs/self-hosting/faq/storage-space.md b/docs/docs/self-hosting/faq/storage-space.md deleted file mode 100644 index f1ad78c71..000000000 --- a/docs/docs/self-hosting/faq/storage-space.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Increase storage space -description: Increasing the storage quota for users on your self hosted instance ---- - -# Increase storage space - -See the [guide for administering your server](/self-hosting/guides/admin). In -particular, you can use the `ente admin update-subscription` CLI command to -increase the -[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) -of accounts on your instance. diff --git a/docs/docs/self-hosting/guides/external-s3.md b/docs/docs/self-hosting/guides/external-s3.md index 40b401724..505ae6fe9 100644 --- a/docs/docs/self-hosting/guides/external-s3.md +++ b/docs/docs/self-hosting/guides/external-s3.md @@ -17,6 +17,14 @@ have the keys and secrets for the S3 bucket. The plan is as follows: 4. Create an account and increase storage quota 5. Fix potential CORS issue with your bucket +> [!NOTE] +> +> This is a community contributed guide, and some of these steps might be out of +> sync with the upstream documentation. If something is not working correctly, +> please also see the latest +> [READMEs](https://github.com/ente-io/ente/blob/main/server/README.md) in the +> repository and/or other guides in [self-hosting](/self-hosting/). + ## 1. Create a `compose.yaml` file After cloning the main repository with @@ -25,6 +33,7 @@ After cloning the main repository with git clone https://github.com/ente-io/ente.git # Or git clone git@github.com:ente-io/ente.git cd ente +git submodule update --init --recursive ``` Create a `compose.yaml` file at the root of the project with the following diff --git a/docs/docs/self-hosting/index.md b/docs/docs/self-hosting/index.md index 53db6ab29..c1ae7075e 100644 --- a/docs/docs/self-hosting/index.md +++ b/docs/docs/self-hosting/index.md @@ -24,6 +24,11 @@ cd ente/server docker compose up --build ``` +> [!TIP] +> +> You can also use a pre-built Docker image from `ghcr.io/ente-io/server` ([More +> info](https://github.com/ente-io/ente/blob/main/server/docs/docker.md)) + Then in a separate terminal, you can run (e.g) the web client ```sh @@ -42,7 +47,7 @@ For the mobile apps, you don't even need to build, and can install normal Ente apps and configure them to use your [custom self-hosted server](guides/custom-server/). -> If you want to build from source, see the instructions +> If you want to build the mobile apps from source, see the instructions > [here](guides/mobile-build). ## Next steps diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 000000000..a0090b46d --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,2 @@ +# macOS +.DS_Store diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 000000000..e1ad6de6e --- /dev/null +++ b/infra/README.md @@ -0,0 +1,8 @@ +# Infra + +Various knick-knacks that we use when hosting our servers. + +These are not needed for running Ente's server or for self-hosting, these are +just additional services we run to make our infrastructure more robust. As such, +it's unlikely that you'll find the pieces here directly useful for your needs, +but feel free to have a look around if you're curious! diff --git a/infra/copycat-db/.gitignore b/infra/copycat-db/.gitignore new file mode 100644 index 000000000..75236ad1c --- /dev/null +++ b/infra/copycat-db/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +copycat-db.env diff --git a/infra/copycat-db/Dockerfile b/infra/copycat-db/Dockerfile new file mode 100644 index 000000000..4328ef17f --- /dev/null +++ b/infra/copycat-db/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:latest + +RUN apt-get update && apt-get install -y curl gnupg +RUN apt-get install -y tini + +# Install pg_dump (via Postgres client) +# https://www.postgresql.org/download/linux/ubuntu/ +# +# We don't need it for production backups, but this is useful for local testing. +RUN \ + apt-get install -y lsb-release && \ + sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ + apt-get update && \ + apt-get -y install postgresql-client-12 + +# Install SCW CLI +# Latest release: https://github.com/scaleway/scaleway-cli/releases/latest +RUN \ + export VERSION="2.26.0" && \ + curl -o /usr/local/bin/scw -L "https://github.com/scaleway/scaleway-cli/releases/download/v${VERSION}/scaleway-cli_${VERSION}_linux_amd64" && \ + chmod +x /usr/local/bin/scw + +RUN apt-get install -y jq + +# Install rclone +RUN apt-get install -y unzip +RUN curl https://rclone.org/install.sh | bash + +COPY src / + +ENTRYPOINT ["tini", "--"] + +CMD [ "/backup.sh" ] diff --git a/infra/copycat-db/README.md b/infra/copycat-db/README.md new file mode 100644 index 000000000..266bd4e6e --- /dev/null +++ b/infra/copycat-db/README.md @@ -0,0 +1,172 @@ +# Copycat DB + +Copycat DB is a [service](../services/README.md) to take a backup of our +database. It uses the Scaleway CLI to take backups of the database, and uploads +them to an offsite bucket. + +This bucket has an object lock configured, so backups cannot be deleted before +expiry. Conversely, the service also deletes backups older than some threshold +when it creates a new one to avoid indefinite retention. + +In production the service runs as a cron job, scheduled using a systemd timer. + +> These backups are in addition to the regular snapshots that we take, and are +> meant as a second layer of replication. For more details, see our +> [Reliability and Replication Specification](https://ente.io/reliability). + +## Quick help + +View service status (it gets invoked as a timer automatically, doesn't need to +be started/stopped manually): + +```sh +sudo systemctl status copycat-db +``` + +View logs locally (they'll also be available on Grafana): + +```sh +sudo tail /root/var/logs/copycat-db.log +``` + +## Name + +The name copycat-db is a riff on "copycat", which is what we call our museum +instance that does the object replication. This one replicates the DB, so, +copycat-db. + +## Required environment variables + +##### SCW_CONFIG_PATH + +Path to the `config.yaml` used by Scaleway CLI. + +This contains the credentials and the default region to use when trying to +create and download the database dump. + +If needed, this config file can be generated by running the following commands +on a shell prompt in the container (using `./test.sh sh`) + + scw init + scw config dump + +##### SCW_RDB_INSTANCE_ID + +The UUID of the Scalway RDB instance that we wish to backup. If this is missing, +then the Docker image falls back to using `pg_dump` (as outlined next). + +##### PGUSER, PGPASSWORD, PGHOST + +Not needed in production when taking a backup (since we use the Scaleway CLI to +take backups in production). + +These are used when testing a backup using `pg_dump`, and when restoring +backups. + +##### RCLONE_CONFIG + +Location of the config file, that contains the destination bucket where you want +to use to save the backups, and the credentials to to access it. + +Specifically, the config file contains two remotes: + +- The bucket itself, where data will be stored. + +- A "crypt" remote that wraps the bucket by applying client side encryption. + +The configuration file will contain (lightly) obfuscated versions of the +password, and as long as we have the configuration file we can continue using +rclone to download and decrypt the plaintext. Still, it is helpful to retain the +original password too separately so that the file can be recreated if needed. + +A config file can be generated using `./test.sh sh` + + rclone config + rclone config show + +When generating the config, we keep file (and directory) name encryption off. + +Note that rclone creates a backup of the config file, so Docker needs to have +write access to the directory where it is mounted. + +##### RCLONE_DESTINATION + +Name of the (crypt) remote to which the dump should be saved. Example: +`db-backup-crypt:`. + +Note that this will not include the bucket - the bucket name will be part of the +remote that the crypt remote wraps. + +##### Logging + +The service logs to its standard out/error. The systemd unit is configured to +route these to `/var/logs/copycat-db.log`. + +## Local testing + +The provided `test.sh` script can be used to do a smoke test for building and +running the image. For example, + + ./test.sh bin/bash + +gives us a shell prompt inside the built and running container. + +For more thorough testing, run this service as part of a local test-cluster. + +## Restoring + +The service also knows how to restore the latest backup into a Postgres +instance. This functionality by a separate service (Phoenix) to periodically +verify that the backups are restorable. + +To invoke this, use "./restore.sh" as the command when running the container +(e.g. `./test.sh ./restore.sh`). This will restore the latest backup into the +Postgres instance whose credentials are provided via the various `PG*` +environment variables. + +## Preparing the bucket + +The database dumps are stored in a bucket that has object lock enabled +(compliance mode), and has a default bucket level retention time of 30 days. + +## Deploying + +Ensure that promtail is running, and is configured to scrape +`/root/var/logs/copycat-db.log`. + +Create that the config and log destination directories + + sudo mkdir -p /root/var/config/scw + sudo mkdir -p /root/var/config/rclone + sudo mkdir -p /root/var/logs + +Create the env, scw and rclone configuration files + + sudo tee /root/copycat-db.env + sudo tee /root/var/config/scw/copycat-db-config.yaml + sudo tee /root/var/config/rclone/copycat-db-rclone.conf + +Add the service definition, and start the service + + scp copycat-db.{service,timer} instance: + + sudo mv copycat-db.{service,timer} /etc/systemd/system + sudo systemctl daemon-reload + +To start the cron job + + sudo systemctl start copycat-db.timer + +The timer will trigger the service on the specified schedule. In addition, if +you wish to force the job to service immediately + + sudo systemctl start copycat-db.service + +## Updating + +To update, run the +[GitHub workflow](../../.github/workflows/copycat-db-release.yaml) to build and +push the latest image to our Docker Registry, then restart the systemd service +on the instance + + sudo systemctl restart copycat-db diff --git a/infra/copycat-db/copycat-db.sample.env b/infra/copycat-db/copycat-db.sample.env new file mode 100644 index 000000000..ba557714e --- /dev/null +++ b/infra/copycat-db/copycat-db.sample.env @@ -0,0 +1,8 @@ +SCW_CONFIG_PATH=/var/config/scw/copycat-db-config.yaml +SCW_RDB_INSTANCE_ID= +RCLONE_CONFIG=/var/config/rclone/copycat-db-rclone.conf +RCLONE_DESTINATION=db-backup-crypt: +PGUSER= +PGPASSWORD= +PGHOST=host.docker.internal +PGPORT= diff --git a/infra/copycat-db/copycat-db.service b/infra/copycat-db/copycat-db.service new file mode 100644 index 000000000..d3ec6c485 --- /dev/null +++ b/infra/copycat-db/copycat-db.service @@ -0,0 +1,20 @@ +[Unit] +Documentation=https://github.com/ente-io/ente/blob/main/infra/copycat-db +Requires=docker.service +After=docker.service + +[Service] +Restart=always +RestartSec=3600s +# Don't automatically restart if it fails more than 6 times in 24 hours. +StartLimitInterval=86400 +StartLimitBurst=6 +ExecStartPre=docker pull rg.fr-par.scw.cloud/ente/copycat-db +ExecStartPre=-docker stop copycat-db +ExecStartPre=-docker rm copycat-db +ExecStart=docker run --name copycat-db \ + --env-file /root/copycat-db.env \ + -v /root/var:/var \ + rg.fr-par.scw.cloud/ente/copycat-db +StandardOutput=append:/root/var/logs/copycat-db.log +StandardError=inherit diff --git a/infra/copycat-db/copycat-db.timer b/infra/copycat-db/copycat-db.timer new file mode 100644 index 000000000..c3f6e2e86 --- /dev/null +++ b/infra/copycat-db/copycat-db.timer @@ -0,0 +1,8 @@ +[Unit] +Description=Schedule copycat-db + +[Timer] +OnCalendar=Daily + +[Install] +WantedBy=timers.target diff --git a/infra/copycat-db/src/backup.sh b/infra/copycat-db/src/backup.sh new file mode 100755 index 000000000..f197f4b0f --- /dev/null +++ b/infra/copycat-db/src/backup.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + +NOWS="$(date +%s)" +BACKUP_FILE="db-$NOWS.custom" + +# Scaleway backup names cannot contain dots +BACKUP_NAME="db-$NOWS-custom" + +# Calculate an expiry time 1 month from now +EXPIRYS="$(( 30 * 24 * 60 * 60 + $NOWS ))" + +# Convert it to the ISO 8601 format that SCW CLI understands +# Note that GNU date uses "-d" and an "@" to pass an epoch (macOS uses "-r"). +EXPIRY="$(date -Iseconds --utc --date "@$EXPIRYS")" + +if test -z "$SCW_RDB_INSTANCE_ID" +then + # A required SCW related environment variable hasn't been specified. This is + # expected when running the script locally for testing. Fallback to using + # pg_dump for creating the backup. + pg_dump -Fc ente_db > $BACKUP_FILE +else + # We need to export a backup first after creating it, before it can be + # downloaded. + # + # Further, our backups currently take longer than the default 20 minute + # timeout for the export set by Scaleway, and end up failing: + # + # {"error":"scaleway-sdk-go: waiting for database backup failed: timeout after 20m0s"} + # + # To avoid this we need to add a custom wait here ourselves instead of using + # the convenience `--wait` flag for the export command provided by Scaleway. + 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 download output=$BACKUP_FILE \ + $(scw rdb backup export $BACKUP_ID --wait -o json | jq -r '.id') +fi + +rclone copy --log-level INFO $BACKUP_FILE $RCLONE_DESTINATION + +# Delete older backups +rclone delete --log-level INFO --min-age 30d $RCLONE_DESTINATION + +set +o xtrace +echo "copycat-db: backup complete: $BACKUP_FILE" diff --git a/infra/copycat-db/src/restore.sh b/infra/copycat-db/src/restore.sh new file mode 100755 index 000000000..8df19c62b --- /dev/null +++ b/infra/copycat-db/src/restore.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + +# Find the name of the latest backup +# The backup file name contains the epoch, so we can just sort. +BACKUP_FILE=$(rclone lsf --include 'db-*.custom' --files-only $RCLONE_DESTINATION | sort | tail -1) + +# Download it +rclone copy --log-level INFO "${RCLONE_DESTINATION}${BACKUP_FILE}" . + +# Restore from it +# +# This create a database named rdb on Postgres - this is only used for the +# initial connection, the actual ente_db database will be created once the +# restore starts. +# +# Flags: +# +# * no-owner: recreates the schema using the current user, not the one that was +# used for the export. +# +# * no-privileges: skip the assignment of roles (this way we do not have to +# recreate all the users from the original database before proceeding with the +# restore) + +createdb rdb || true +pg_restore -d rdb --create --no-privileges --no-owner --exit-on-error "$BACKUP_FILE" + +# Delete any tokens that were in the backup +psql -d ente_db -c 'delete from tokens' + +# Delete any push tokens that were in the backup +psql -d ente_db -c 'delete from push_tokens' + +# Delete some more temporary data that might've come up in the backup +psql -d ente_db -c 'delete from queue' +psql -d ente_db -c 'delete from temp_objects' + +set +o xtrace +echo "copycat-db: restore complete: $BACKUP_FILE" diff --git a/infra/copycat-db/test.sh b/infra/copycat-db/test.sh new file mode 100755 index 000000000..d4ac1b35f --- /dev/null +++ b/infra/copycat-db/test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -o xtrace +set -o errexit + +PROJECT=copycat-db + +docker rmi "ente/$PROJECT" || true +docker build --tag "ente/$PROJECT" . + +# Interactively run the container. +# +# By passing "$@", we allow any arguments passed to test.sh to be forwarded to +# the image (useful for testing out things, e.g. `./test.sh sh`). +docker run \ + --interactive --tty --rm \ + --env-file copycat-db.env \ + --name "$PROJECT" \ + "ente/$PROJECT" \ + "$@" diff --git a/infra/services/README.md b/infra/services/README.md new file mode 100644 index 000000000..1c3c6a368 --- /dev/null +++ b/infra/services/README.md @@ -0,0 +1,113 @@ +# Services + +"Services" are Docker images we run on our instances and manage using systemd. + +All our services (including museum itself) follow the same pattern: + +- They're run on vanilla Ubuntu instances. The only expectation they have is + for Docker to be installed. + +- They log to fixed, known, locations - `/root/var/log/foo.log` - so that + these logs can get ingested by Promtail if needed. + +- Each service should consist of a Docker image (or a Docker compose file), + and a systemd unit file. + +- To start / stop / schedule the service, we use systemd. + +- Each time the service runs it should pull the latest Docker image, so there + is no separate installation/upgrade step needed. We can just restart the + service, and it'll use the latest code. + +- Any credentials and/or configuration should be read by mounting the + appropriate file from `/root/service-name` into the running Docker + container. + +## Systemd cheatsheet + +```sh +sudo systemctl status my-service +sudo systemctl start my-service +sudo systemctl stop my-service +sudo systemctl restart my-service +sudo journalctl --unit my-service +``` + +## Adding a service + +Create a systemd unit file (See the various `*.service` files in this repository +for examples). + +If we want the service to start on boot, add an `[Install]` section to its +service file (_note_: starting on boot requires one more step later): + +``` +[Install] +WantedBy=multi-user.target +``` + +Copy the service file to the instance where we want to run the service. Services +might also have some additional configuration or env files, also copy those to +the instance. + +```sh +scp services/example.service example.env : +``` + +SSH into the instance. + +```sh +ssh +``` + +Move the service `/etc/systemd/service`, and any config files to their expected +place. env and other config files that contain credentials are kept in `/root`. + +```sh +sudo mv example.service /etc/systemd/system +sudo mv example.env /root +``` + +If you want to start the service on boot (as spoken of in the `[Install]` +section above), then enable it (this only needs to be done once): + +```sh +sudo systemctl enable service +``` + +Restarts systemd so that it gets to know of the service. + +```sh +sudo systemctl daemon-reload +``` + +Now you can manage the service using standard systemd commands. + +```sh +sudo systemctl start example +``` + +To view stdout/err, use: + +```sh +sudo journalctl --follow --unit example +``` + +## Logging + +Simple services can log to their standard output: these are captured by Docker, +and by default promtail is setup to injest Docker logs and send them to Grafana. + +One issue with the above simple setup is that we cannot attach job names. + +If the service needs to to attach a specific job name, or if the service wants +more control over the log retention etc, then then services can log to to its +own files. + +* Such files should be in `/var/logs` within the container, and this should be + mounted to `/root/var/logs` on the instance (using the `-v` flag in the + service file which launches the Docker container or the Docker compose cluster). + +* There should be entry for this log file in the `promtail/promtail.yaml` on + that instance. The logs will then get scraped by Promtail and sent over to + Grafana. diff --git a/infra/services/nginx/README.md b/infra/services/nginx/README.md new file mode 100644 index 000000000..7239a5610 --- /dev/null +++ b/infra/services/nginx/README.md @@ -0,0 +1,64 @@ +# Nginx + +This is a base Nginx service that terminates TLS, and can be used as a reverse +proxy for arbitrary services by adding new entries in `/root/nginx/conf.d` and +`sudo systemctl reload nginx`. + +## Installation + +Copy the service definition + +```sh +scp services/nginx/nginx.service : + +sudo mv nginx.service /etc/systemd/system/nginx.service +``` + +Create a directory to house service specific configuration + +```sh +sudo mkdir -p /root/nginx/conf.d +``` + +Add the SSL certificate provided by Cloudflare + +```sh +sudo tee /root/nginx/cert.pem +sudo tee /root/nginx/key.pem +``` + +Tell systemd to pick up new service definition, enable it (so that it +automatically starts on boot going forward), and start it. + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now nginx +``` + +## Adding a service + +When adding new services that sit behind Nginx, + +1. Add its nginx conf file to `/root/nginx/conf.d` + +2. Restart nginx (`sudo systemctl reload nginx`) + +## Configuration files + +All the files we put into `/root/nginx/conf.d` get included in an `http` block. +We can see this in the default configuration of nginx: + + http { + ... + include /etc/nginx/conf.d/*.conf; + } + +> To view the default configuration, run the following command against the +> [official Docker image for Nginx](https://hub.docker.com/_/nginx), which is +> also what we use: +> +> docker run --rm --entrypoint=cat nginx /etc/nginx/nginx.conf > /tmp/nginx.conf + +This is a [handy tool](https://nginx-playground.wizardzines.com) to check the +syntax of the configuration files. Alternatively, you can run `docker exec nginx +nginx -t` on the instance to ask nginx to check the configuration. diff --git a/infra/services/nginx/nginx.service b/infra/services/nginx/nginx.service new file mode 100644 index 000000000..e14e7840d --- /dev/null +++ b/infra/services/nginx/nginx.service @@ -0,0 +1,20 @@ +[Unit] +Documentation=https://www.docker.com/blog/how-to-use-the-official-nginx-docker-image/ +Requires=docker.service +After=docker.service + +[Install] +WantedBy=multi-user.target + +[Service] +ExecStartPre=docker pull nginx +ExecStartPre=-docker stop nginx +ExecStartPre=-docker rm nginx +ExecStart=docker run --name nginx \ + --add-host=host.docker.internal:host-gateway \ + -p 443:443 \ + -v /root/nginx/cert.pem:/etc/ssl/certs/cert.pem:ro \ + -v /root/nginx/key.pem:/etc/ssl/private/key.pem:ro \ + -v /root/nginx/conf.d:/etc/nginx/conf.d:ro \ + nginx +ExecReload=docker exec nginx nginx -s reload diff --git a/infra/services/prometheus/README.md b/infra/services/prometheus/README.md new file mode 100644 index 000000000..8f05f7fb9 --- /dev/null +++ b/infra/services/prometheus/README.md @@ -0,0 +1,34 @@ +# Prometheus + +Install `prometheus.service` on an instance if it is running something that +exports custom Prometheus metrics. In particular, museum does. + +If it is an instance whose metrics (CPU, disk, RAM etc) we want to monitor, also +install `node-exporter.service` after installing +[node-exporter](https://prometheus.io/docs/guides/node-exporter/) itself (Note +that our prepare-instance script already installs node-exporter) . + +## Installing + +Prometheus doesn't currently support environment variables in config file, so +remember to change the hardcoded `XX-HOSTNAME` too in addition to adding the +`remote_write` configuration. + +```sh +scp services/prometheus/prometheus.* : +scp services/prometheus/node-exporter.service : + +nano prometheus.yml +sudo mv prometheus.yml /root/prometheus.yml +sudo mv prometheus.service /etc/systemd/system/prometheus.service +sudo mv node-exporter.service /etc/systemd/system/node-exporter.service +``` + +Tell systemd to pick up new service definitions, enable the units (so that they +automatically start on boot going forward), and start them. + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now node-exporter +sudo systemctl enable --now prometheus +``` diff --git a/infra/services/prometheus/node-exporter.service b/infra/services/prometheus/node-exporter.service new file mode 100644 index 000000000..b28e348ae --- /dev/null +++ b/infra/services/prometheus/node-exporter.service @@ -0,0 +1,12 @@ +[Unit] +Documentation=https://prometheus.io/docs/guides/node-exporter/ +Wants=network-online.target +After=network-online.target + +[Install] +WantedBy=multi-user.target + +[Service] +User=node_exporter +Group=node_exporter +ExecStart=/usr/local/bin/node_exporter diff --git a/infra/services/prometheus/prometheus.service b/infra/services/prometheus/prometheus.service new file mode 100644 index 000000000..a8f6c48f0 --- /dev/null +++ b/infra/services/prometheus/prometheus.service @@ -0,0 +1,16 @@ +[Unit] +Documentation=https://prometheus.io/docs/prometheus/ +Requires=docker.service +After=docker.service + +[Install] +WantedBy=multi-user.target + +[Service] +ExecStartPre=docker pull prom/prometheus +ExecStartPre=-docker stop prometheus +ExecStartPre=-docker rm prometheus +ExecStart=docker run --name prometheus \ + --add-host=host.docker.internal:host-gateway \ + -v /root/prometheus.yml:/etc/prometheus/prometheus.yml:ro \ + prom/prometheus diff --git a/infra/services/prometheus/prometheus.yml b/infra/services/prometheus/prometheus.yml new file mode 100644 index 000000000..81d1e3d84 --- /dev/null +++ b/infra/services/prometheus/prometheus.yml @@ -0,0 +1,39 @@ +# https://prometheus.io/docs/prometheus/latest/configuration/ + +global: + scrape_interval: 30s # Default is 1m + +scrape_configs: + - job_name: museum + static_configs: + - targets: ["host.docker.internal:2112"] + relabel_configs: + - source_labels: [__address__] + regex: ".*" + target_label: instance + replacement: XX-HOSTNAME + + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + relabel_configs: + - source_labels: [__address__] + regex: ".*" + target_label: instance + replacement: XX-HOSTNAME + + - job_name: "node" + static_configs: + - targets: ["host.docker.internal:9100"] + relabel_configs: + - source_labels: [__address__] + regex: ".*" + target_label: instance + replacement: XX-HOSTNAME + +# Grafana Cloud +remote_write: + - url: https://g/api/prom/push + basic_auth: + username: foo + password: bar diff --git a/infra/services/promtail/README.md b/infra/services/promtail/README.md new file mode 100644 index 000000000..9a12b10c3 --- /dev/null +++ b/infra/services/promtail/README.md @@ -0,0 +1,25 @@ +# Promtail + +Install `promtail.service` on an instance if it is running something whose logs +we want in Grafana. + +## Installing + +Replace `client.url` in the config file with the Loki URL that Promtail should +connect to, and move the files to their expected place. + +```sh +scp services/promtail/promtail.* : + +nano promtail.yaml +sudo mv promtail.yaml /root/promtail.yaml +sudo mv promtail.service /etc/systemd/system/promtail.service +``` + +Tell systemd to pick up new service definitions, enable the unit (so that it +automatically starts on boot), and start it this time around. + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now promtail +``` diff --git a/infra/services/promtail/promtail.service b/infra/services/promtail/promtail.service new file mode 100644 index 000000000..b98ec762f --- /dev/null +++ b/infra/services/promtail/promtail.service @@ -0,0 +1,19 @@ +[Unit] +Documentation=https://grafana.com/docs/loki/latest/clients/promtail/ +Requires=docker.service +After=docker.service + +[Install] +WantedBy=multi-user.target + +[Service] +ExecStartPre=docker pull grafana/promtail +ExecStartPre=-docker stop promtail +ExecStartPre=-docker rm promtail +ExecStart=docker run --name promtail \ + --hostname "%H" \ + -v /root/promtail.yaml:/config.yaml:ro \ + -v /var/log:/var/log \ + -v /root/var/logs:/var/logs:ro \ + -v /var/lib/docker/containers:/var/lib/docker/containers:ro \ + grafana/promtail -config.file=/config.yaml -config.expand-env=true diff --git a/infra/services/promtail/promtail.yaml b/infra/services/promtail/promtail.yaml new file mode 100644 index 000000000..99a2578ff --- /dev/null +++ b/infra/services/promtail/promtail.yaml @@ -0,0 +1,45 @@ +# https://grafana.com/docs/loki/latest/clients/promtail/configuration/ + +# We don't want Promtail's HTTP / GRPC server. +server: + disable: true + +# Loki URL +# For Grafana Cloud, it can be found in the integrations section. +clients: + - url: http://loki:3100/loki/api/v1/push + +# Manually add entries for all our services. This is a bit cumbersome, but +# - Retains flexibility in file names. +# - Makes adding job labels easy. +# - Does not get in the way of logrotation. +# +# In addition, also scrape logs from all docker containers. +scrape_configs: + - job_name: museum + static_configs: + - labels: + job: museum + host: ${HOSTNAME} + __path__: /var/logs/museum.log + + - job_name: copycat-db + static_configs: + - labels: + job: copycat-db + host: ${HOSTNAME} + __path__: /var/logs/copycat-db.log + + - job_name: phoenix + static_configs: + - labels: + job: phoenix + host: ${HOSTNAME} + __path__: /var/logs/phoenix.log + + - job_name: docker + static_configs: + - labels: + job: docker + host: ${HOSTNAME} + __path__: /var/lib/docker/containers/*/*-json.log diff --git a/infra/services/status/README.md b/infra/services/status/README.md new file mode 100644 index 000000000..78d12001e --- /dev/null +++ b/infra/services/status/README.md @@ -0,0 +1,43 @@ +# Status + +Our status page ([status.ente.io](https://status.ente.io)) is a self-hosted +[Uptime Kuma](https://github.com/louislam/uptime-kuma). + +## Installing + +Install [nginx](../nginx/README.md). + +Create a directory where Uptime Kuma will keep its state. This is the directory +we can optionally backup if we wish to preserve history and settings when moving +instances in the future. + +```sh +sudo mkdir -p /root/uptime-kuma +``` + +Add the service definition and nginx configuration. + +```sh +scp services/status/uptime-kuma.* : + +sudo mv uptime-kuma.service /etc/systemd/system/ +sudo mv uptime-kuma.nginx.conf /root/nginx/conf.d +``` + +Tell systemd to pick up new service definitions, enable the unit (so that it +automatically starts on boot), and start it this time around. + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now uptime-kuma +``` + +Tell nginx to pick up the new configuration. + +```sh +sudo systemctl reload nginx +``` + +## Administration + +Login into the [dashboard](https://status.ente.io/dashboard) for administration. diff --git a/infra/services/status/uptime-kuma.nginx.conf b/infra/services/status/uptime-kuma.nginx.conf new file mode 100644 index 000000000..c45c7b660 --- /dev/null +++ b/infra/services/status/uptime-kuma.nginx.conf @@ -0,0 +1,26 @@ +# This file gets loaded in a top level http block by the default nginx.conf +# See infra/services/nginx/README.md for more details. + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + server_name status.ente.io; + + location / { + proxy_pass http://host.docker.internal:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Recommended options from Uptime Kuma Wiki for Websockets. + # + # https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy#nginx + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/infra/services/status/uptime-kuma.service b/infra/services/status/uptime-kuma.service new file mode 100644 index 000000000..11789d8c4 --- /dev/null +++ b/infra/services/status/uptime-kuma.service @@ -0,0 +1,16 @@ +[Unit] +Documentation=https://github.com/louislam/uptime-kuma +Requires=docker.service +After=docker.service + +[Install] +WantedBy=multi-user.target + +[Service] +ExecStartPre=docker pull louislam/uptime-kuma:1 +ExecStartPre=-docker stop uptime-kuma +ExecStartPre=-docker rm uptime-kuma +ExecStart=docker run --name uptime-kuma \ + -p 3001:3001 \ + -v /root/uptime-kuma:/app/data \ + louislam/uptime-kuma:1 diff --git a/infra/workers/.gitignore b/infra/workers/.gitignore new file mode 100644 index 000000000..7f811e128 --- /dev/null +++ b/infra/workers/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +.DS_Store +.wrangler/ + +# Workers mostly have dev dependencies, and usually only wrangler, so exact +# version details and the resulting `yarn.lock` is unnecessary noise. +# +# If we need to pin some runtime dependency, we can pin it to an exact version +# in the package.json itself. +yarn.lock diff --git a/infra/workers/.prettierrc.json b/infra/workers/.prettierrc.json new file mode 100644 index 000000000..0a02bcefd --- /dev/null +++ b/infra/workers/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/infra/workers/README.md b/infra/workers/README.md new file mode 100644 index 000000000..7862045da --- /dev/null +++ b/infra/workers/README.md @@ -0,0 +1,32 @@ +# Cloudflare Workers + +Source code for our [Cloudflare +Workers](https://developers.cloudflare.com/workers/). + +Each worker is a self contained directory with its each `package.json`. + +## Deploying + +* Switch to a worker directory, e.g. `cd github-discord-notifier`. + +* Install dependencies (if needed) with `yarn` + +* Login into wrangler (if needed) using `yarn wrangler login` + +* Deploy! `yarn wrangler deploy` + +Wrangler is the CLI provided by Cloudflare to manage workers. Apart from +deploying, it also allows us to stream logs from running workers by using `yarn +wrangler tail`. + +## Creating a new worker + +Copy paste an existing one. Unironically this is a good option because +Cloudflare's template has a lot of unnecessary noise, but if really do want to +create one from scratch, use `npm create cloudflare@latest`. + +To import an existing worker from the Cloudflare dashboard, use + +```sh +npm create cloudflare@2 existing-worker-name -- --type pre-existing --existing-script existing-worker-name +``` diff --git a/infra/workers/cast-albums/package.json b/infra/workers/cast-albums/package.json new file mode 100644 index 000000000..7d87162d2 --- /dev/null +++ b/infra/workers/cast-albums/package.json @@ -0,0 +1,9 @@ +{ + "name": "cast-albums", + "private": true, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240314.0", + "typescript": "^5", + "wrangler": "^3" + } +} diff --git a/infra/workers/cast-albums/src/index.ts b/infra/workers/cast-albums/src/index.ts new file mode 100644 index 000000000..d76c33375 --- /dev/null +++ b/infra/workers/cast-albums/src/index.ts @@ -0,0 +1,50 @@ +/** Proxy file and thumbnail requests from the cast web app */ + +export default { + async fetch(request: Request) { + switch (request.method) { + case "GET": + return handleGET(request); + case "OPTIONS": + return handleOPTIONS(request); + default: + throw new Error( + `HTTP 405 Method Not Allowed: ${request.method}` + ); + } + }, +} satisfies ExportedHandler; + +const handleGET = async (request: Request) => { + const url = new URL(request.url); + const urlParams = new URLSearchParams(url.search); + const token = + request.headers.get("X-Cast-Access-Token") ?? + urlParams.get("castToken"); + + const fileID = urlParams.get("fileID"); + const pathname = url.pathname; + + let response = await fetch( + `https://api.ente.io/cast/files${pathname}${fileID}?castToken=${token}` + ); + + response = new Response(response.body, response); + response.headers.set("Access-Control-Allow-Origin", "*"); + return response; +}; + +const handleOPTIONS = (request: Request) => { + let corsHeaders: Record = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,OPTIONS", + "Access-Control-Max-Age": "86400", + }; + + const acrh = request.headers.get("Access-Control-Request-Headers"); + if (acrh) { + corsHeaders["Access-Control-Allow-Headers"] = acrh; + } + + return new Response("", { headers: corsHeaders }); +}; diff --git a/infra/workers/cast-albums/tsconfig.json b/infra/workers/cast-albums/tsconfig.json new file mode 100644 index 000000000..70c70a35d --- /dev/null +++ b/infra/workers/cast-albums/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] } diff --git a/infra/workers/cast-albums/wrangler.toml b/infra/workers/cast-albums/wrangler.toml new file mode 100644 index 000000000..c070a5c24 --- /dev/null +++ b/infra/workers/cast-albums/wrangler.toml @@ -0,0 +1,8 @@ +name = "cast-albums" +main = "src/index.ts" +compatibility_date = "2024-03-14" + +[[routes]] +pattern = "cast-albums.ente.io" +zone_name = "ente.io" +custom_domain = true diff --git a/infra/workers/github-discord-notifier/package.json b/infra/workers/github-discord-notifier/package.json new file mode 100644 index 000000000..165c48aea --- /dev/null +++ b/infra/workers/github-discord-notifier/package.json @@ -0,0 +1,9 @@ +{ + "name": "github-discord-notifier", + "private": true, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240314.0", + "typescript": "^5", + "wrangler": "^3" + } +} diff --git a/infra/workers/github-discord-notifier/src/index.ts b/infra/workers/github-discord-notifier/src/index.ts new file mode 100644 index 000000000..4e67c8469 --- /dev/null +++ b/infra/workers/github-discord-notifier/src/index.ts @@ -0,0 +1,100 @@ +/** + * Forward notifications from GitHub to Discord. + * + * This worker receives webhooks from GitHub, filters out the ones we don't + * need, and forwards them to a Discord webhook. + */ +export default { + async fetch(request: Request, env: Env) { + return handleRequest(request, env.DISCORD_WEBHOOK_URL); + }, +} satisfies ExportedHandler; + +interface Env { + DISCORD_WEBHOOK_URL: string; +} + +const handleRequest = async (request: Request, discordWebhookURL: string) => { + const requestBody = await request.text(); + const requestJSON = JSON.parse(requestBody); + const sender = requestJSON["sender"]["login"]; + if (sender === "cloudflare-pages[bot]" || sender === "CLAassistant") { + // Ignore pings from CF bot + return new Response(null, { status: 200 }); + } + + // [Note: GitHub specific Discord Webhooks] + // + // By appending `/github` to the end of the webhook URL, we can get Discord + // to automatically parse the payload sent by GitHub. + // https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook + // + // Note that this doesn't work for all events. And sadly, the events it + // doesn't work for get silently ignored (Discord responds with a 204). + // https://github.com/discord/discord-api-docs/issues/6203#issuecomment-1608151265 + + let response = await fetch(`${discordWebhookURL}/github`, { + method: request.method, + headers: request.headers, + body: requestBody, + }); + + if (response.status === 429) { + // Sometimes Discord starts returning 429 Rate Limited responses when we + // try to invoke the webhook. + // + // Retry-After: 300 + // X-Ratelimit-Global: true + // X-Ratelimit-Scope: global + // + // {"message": "You are being rate limited.", "retry_after": 0.3, "global": true} + // + // This just seems to be a bug on their end, and it goes away on its own + // after a while. My best guess is that the IP of the Cloudflare Worker + // somehow gets rate limited because of someone else trying to spam from + // a worker running on the same IP. But it's a guess. I'm not sure. + // + // Ref: + // https://discord.com/developers/docs/topics/rate-limits#global-rate-limit + // + // Interestingly, this only happens for the `/github` specific webhook. + // The normal webhook still works. So as a workaround, just send a + // normal text message to the webhook when we get a 429. + + // The JSON sent by GitHub has a varied schema. This is a stop-gap + // arrangement (we shouldn't be getting 429s forever), so just try to + // see if we can extract a URL from something we recognize. + let activityURL: string | undefined; + if (requestJSON["comment"]) { + activityURL = requestJSON["comment"]["html_url"]; + } + if (!activityURL && requestJSON["issue"]) { + activityURL = requestJSON["issue"]["html_url"]; + } + if (!activityURL && requestJSON["discussion"]) { + activityURL = requestJSON["discussion"]["html_url"]; + } + + // Ignore things like issue label changes. + const action = requestJSON["action"]; + + if (activityURL && ["created", "opened"].includes(action)) { + response = await fetch(discordWebhookURL, { + method: request.method, + headers: request.headers, + body: JSON.stringify({ + content: `Activity in ${activityURL}`, + }), + }); + } + } + + const responseBody = await response.text(); + const newResponse = new Response(responseBody, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + + return newResponse; +}; diff --git a/infra/workers/github-discord-notifier/tsconfig.json b/infra/workers/github-discord-notifier/tsconfig.json new file mode 100644 index 000000000..70c70a35d --- /dev/null +++ b/infra/workers/github-discord-notifier/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] } diff --git a/infra/workers/github-discord-notifier/wrangler.toml b/infra/workers/github-discord-notifier/wrangler.toml new file mode 100644 index 000000000..5cc16fb61 --- /dev/null +++ b/infra/workers/github-discord-notifier/wrangler.toml @@ -0,0 +1,7 @@ +name = "github-discord-notifier" +main = "src/index.ts" +compatibility_date = "2024-03-14" + +[vars] +# Added as a secret via the Cloudflare dashboard +# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/{webhook.id}/{webhook.token}" diff --git a/infra/workers/tsconfig.base.json b/infra/workers/tsconfig.base.json new file mode 100644 index 000000000..f330c4ca5 --- /dev/null +++ b/infra/workers/tsconfig.base.json @@ -0,0 +1,38 @@ +{ + /* A shared TSConfig for use by TypeScript Cloudflare Workers */ + /* TSConfig docs: https://aka.ms/tsconfig.json */ + "compilerOptions": { + /* tsc is used for by us for type checking, not compilation (the + Cloudflare workers runtime natively supports TypeScript) */ + "noEmit": true, + + /* The Workers runtime supports the latest and greatest */ + /* https://developers.cloudflare.com/workers/reference/languages/#javascript--typescript */ + "lib": ["esnext"], + "target": "esnext", + "module": "esnext", + + /* Types that are implicitly available */ + /* https://www.npmjs.com/package/@cloudflare/workers-types */ + "types": ["@cloudflare/workers-types"], + + /* Tell TypeScript how to lookup the file for a given import */ + "moduleResolution": "node", + + /* Speed things up by not type checking `node_modules` */ + "skipLibCheck": true, + /* Require the `type` modifier when importing types */ + "verbatimModuleSyntax": true, + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* strict and then some */ + "strict": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true + } +} diff --git a/mobile/CHANGELOG.md b/mobile/CHANGELOG.md index 445bb9147..167b65b6e 100644 --- a/mobile/CHANGELOG.md +++ b/mobile/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## v0.8.73 +### Added +* #### Share an Album to Multiple Contacts at Once + + Adding multiple viewers and collaborators just got easier! + You can now select multiple contacts and add all of them at once. + +* #### Bug Fixes and Performance Improvements + + Many a bugs were squashed in this release and have improved performance on app start. If you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏 + ## v0.8.67 @@ -85,22 +96,3 @@ If you would like to help us improve ente, come join the party @ ente.io/community! -## v0.7.71 - -### Added -* #### Map View ✨ - - You can now explore the photos you've taken around the world! - - Click on the Map icon on the Search screen to view your photos laid out on a map. - -* #### Cover Photos ✨ - You can now set cover photos for your albums. - - Open an album, and click on the overflow menu on the top right corner to pick your favorite memory from that album. - -### Improvements - -* **Translations**: Add support for German language -* This release contains massive improvements to how smoothly our gallery - scrolls. More improvements are on the way! diff --git a/mobile/README.md b/mobile/README.md index 114a3ab38..005d303b6 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -23,7 +23,8 @@ If you're looking for Ente Auth instead, see [../auth](../auth/README.md). ### Android -The [GitHub releases](https://github.com/ente-io/photos-app/releases) contain +The [GitHub +releases](https://github.com/ente-io/ente/releases?q=tag%3Aphotos-v0) contain APKs, built straight from source. The latest build is available at [ente.io/apk](https://ente.io/apk). These builds keep themselves updated, without relying on third party stores. diff --git a/mobile/fastlane/metadata/android/de/full_description.txt b/mobile/fastlane/metadata/android/de/full_description.txt index 2d953ade9..4d963c1d7 100644 --- a/mobile/fastlane/metadata/android/de/full_description.txt +++ b/mobile/fastlane/metadata/android/de/full_description.txt @@ -27,7 +27,7 @@ FEATURES - und noch VIELES mehr! BERECHTIGUNGEN -Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PREIS Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden. diff --git a/mobile/fastlane/metadata/android/en-US/full_description.txt b/mobile/fastlane/metadata/android/en-US/full_description.txt index 53cab6c96..9ba4fe314 100644 --- a/mobile/fastlane/metadata/android/en-US/full_description.txt +++ b/mobile/fastlane/metadata/android/en-US/full_description.txt @@ -27,7 +27,7 @@ FEATURES - 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/photos-app/blob/f-droid/android/permissions.md +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. diff --git a/mobile/fastlane/metadata/android/es/full_description.txt b/mobile/fastlane/metadata/android/es/full_description.txt index dfba04a96..8c0667934 100644 --- a/mobile/fastlane/metadata/android/es/full_description.txt +++ b/mobile/fastlane/metadata/android/es/full_description.txt @@ -27,7 +27,7 @@ CARACTERÍSTICAS - ¡Y mucho más! PERMISOS -ente solicita ciertos permisos para servir al propósito de un proveedor de almacenamiento de fotos, que puede ser revisado aquí: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente solicita ciertos permisos para servir al propósito de un proveedor de almacenamiento de fotos, que puede ser revisado aquí: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PRECIOS No ofrecemos planes gratis para siempre, porque es importante para nosotros seguir siendo sostenibles y resistir a la prueba del tiempo. En su lugar, ofrecemos planes asequibles que puedes compartir libremente con tu familia. Puedes encontrar más información en ente.io. diff --git a/mobile/fastlane/metadata/android/fr/full_description.txt b/mobile/fastlane/metadata/android/fr/full_description.txt index ee93cf62b..f94e467af 100644 --- a/mobile/fastlane/metadata/android/fr/full_description.txt +++ b/mobile/fastlane/metadata/android/fr/full_description.txt @@ -27,7 +27,7 @@ CARACTÉRISTIQUES - et beaucoup de choses encore ! PERMISSIONS -ente sollicite diverses autorisations dans le but de fonctionner en tant que service de stockage de photos, et ces autorisations sont détaillées ici : https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente sollicite diverses autorisations dans le but de fonctionner en tant que service de stockage de photos, et ces autorisations sont détaillées ici : https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PRIX Nous ne proposons pas d'abonnement gratuits pour toujours, car il est important pour nous de rester durables et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io. diff --git a/mobile/fastlane/metadata/android/he/full_description.txt b/mobile/fastlane/metadata/android/he/full_description.txt index 094285850..21719b771 100644 --- a/mobile/fastlane/metadata/android/he/full_description.txt +++ b/mobile/fastlane/metadata/android/he/full_description.txt @@ -27,7 +27,7 @@ ente גם מקל על שיתוף האלבומים שלך עם קרובך, גם - ועוד הרבה יותר! הרשאות -ente מבקש הרשאות מסוימות כדי לספק שירותי אחסון תמונות, וניתן לסקור אותן כאן: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente מבקש הרשאות מסוימות כדי לספק שירותי אחסון תמונות, וניתן לסקור אותן כאן: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md מחיר אנחנו לא מציעים תוכניות בחינם לתמיד, משום שזה חשוב לנו להיות עמידים ולעמוד במבחן הזמן. במקום זאת אנחנו מציעים תוכניות במחיר סביר כדי שתוכל לשתף באופן חופשי עם המשפחה שלך. ניתן למצוא עוד מידע ב-ente.io. diff --git a/mobile/fastlane/metadata/android/it/full_description.txt b/mobile/fastlane/metadata/android/it/full_description.txt index 2437688c5..9da92c97a 100644 --- a/mobile/fastlane/metadata/android/it/full_description.txt +++ b/mobile/fastlane/metadata/android/it/full_description.txt @@ -27,7 +27,7 @@ CARATTERISTICHE - e molto altro ancora! PERMESSI -ente richiede alcune autorizzazioni per servire lo scopo di un provider di storage fotografico, che può essere esaminato qui: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente richiede alcune autorizzazioni per servire lo scopo di un provider di storage fotografico, che può essere esaminato qui: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PREZZO Non offriamo piani gratuiti per sempre, perché per noi è importante rimanere sostenibili e resistere alla prova del tempo. Offriamo invece piani accessibili che si possono condividere liberamente con la propria famiglia. Puoi trovare maggiori informazioni su ente.io. diff --git a/mobile/fastlane/metadata/android/nl/full_description.txt b/mobile/fastlane/metadata/android/nl/full_description.txt index da0877f72..80c925263 100644 --- a/mobile/fastlane/metadata/android/nl/full_description.txt +++ b/mobile/fastlane/metadata/android/nl/full_description.txt @@ -27,7 +27,7 @@ FUNCTIES - en nog veel meer! TOESTEMMINGEN -ente heeft bepaalde machtigingen nodig om uw foto's op te slaan, die hier bekeken kunnen worden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente heeft bepaalde machtigingen nodig om uw foto's op te slaan, die hier bekeken kunnen worden: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md PRIJZEN We bieden geen oneindig gratis plannen aan, omdat het voor ons belangrijk is dat we duurzaam blijven en de tand des tijds weerstaan. In plaats daarvan bieden we betaalbare plannen aan die je vrij kunt delen met je familie. Je kunt meer informatie vinden op ente.io. diff --git a/mobile/fastlane/metadata/android/pt/full_description.txt b/mobile/fastlane/metadata/android/pt/full_description.txt index 2bd26df1e..8e6bc4833 100644 --- a/mobile/fastlane/metadata/android/pt/full_description.txt +++ b/mobile/fastlane/metadata/android/pt/full_description.txt @@ -27,7 +27,7 @@ RECURSOS - e MUITO MAIS! PERMISSÕES -ente solicita certas permissões para servir o propósito de um provedor de armazenamento de fotos, que pode ser revisado aqui: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente solicita certas permissões para servir o propósito de um provedor de armazenamento de fotos, que pode ser revisado 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 que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io. diff --git a/mobile/fastlane/metadata/android/ru/full_description.txt b/mobile/fastlane/metadata/android/ru/full_description.txt index f374837e0..567f5a947 100644 --- a/mobile/fastlane/metadata/android/ru/full_description.txt +++ b/mobile/fastlane/metadata/android/ru/full_description.txt @@ -27,7 +27,7 @@ ente также делает так, что делится альбомами с - и ещё МНОГОЕ другое! РАЗРЕШЕНИЯ -ente просит разрешения на использование хранилища фотографий, которые можно рассмотреть здесь: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente просит разрешения на использование хранилища фотографий, которые можно рассмотреть здесь: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md ЦЕНА Мы не предлагаем бесконечные бесплатные планы, потому что для нас важно оставаться устойчивыми и выдерживать испытание временем. Вместо этого мы предлагаем доступные по цене планы, которыми вы можете свободно делиться с вашей семьей. Дополнительную информацию можно найти на сайте ente.io. diff --git a/mobile/fastlane/metadata/android/zh/full_description.txt b/mobile/fastlane/metadata/android/zh/full_description.txt index 2410d9da0..2fffa8a0c 100644 --- a/mobile/fastlane/metadata/android/zh/full_description.txt +++ b/mobile/fastlane/metadata/android/zh/full_description.txt @@ -4,7 +4,7 @@ ente 是一个简单的应用程序来备份和分享您的照片和视频。 我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式 (e2ee) 无缝同步。 -ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 权限 +ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 您可以共享公开可见的链接,他们可以在其中查看您的相册并通过向其中添加照片进行协作,即使没有账户或应用程序也是如此。 您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。 我们认真对待子孙后代,并确保您的回忆比您长寿。 @@ -27,7 +27,7 @@ ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他 - 还有更多特色待你发现! 权限 -ente需要特定权限以执行作为图像存储提供商的职责,相关内容可以在此链接查阅:https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente需要特定权限以执行作为图像存储提供商的职责,相关内容可以在此链接查阅:https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md 价格 我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 diff --git a/mobile/gallery_scroll_perf_test.sh b/mobile/gallery_scroll_perf_test.sh new file mode 100755 index 000000000..3faee8c2d --- /dev/null +++ b/mobile/gallery_scroll_perf_test.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Make sure to go through home_gallery_scroll_test.dart and +# fill in email and password. +# Specify destination directory for the perf results in perf_driver.dart. + + +export ENDPOINT="https://api.ente.io" + +flutter drive \ + --driver=test_driver/perf_driver.dart \ + --target=integration_test/home_gallery_scroll_test.dart \ + --dart-define=endpoint=$ENDPOINT \ + --profile --flavor independent \ + --no-dds + +exit $? diff --git a/mobile/integration_test/app_test.dart b/mobile/integration_test/app_test.dart deleted file mode 100644 index b0b16d46d..000000000 --- a/mobile/integration_test/app_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_test/flutter_test.dart"; -import "package:integration_test/integration_test.dart"; -import "package:photos/main.dart" as app; -import "package:scrollable_positioned_list/scrollable_positioned_list.dart"; - -void main() { - group("App test", () { - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - testWidgets("Demo test", (tester) async { - app.main(); - - await tester.pumpAndSettle(const Duration(seconds: 5)); - - await dismissUpdateAppDialog(tester); - - //Automatically clicks the sign in button on the landing page - final signInButton = find.byKey(const ValueKey("signInButton")); - await tester.tap(signInButton); - await tester.pumpAndSettle(); - - //Need to enter email address manually and clicks the login button automatically - final emailInputField = find.byKey(const ValueKey("emailInputField")); - final logInButton = find.byKey(const ValueKey("logInButton")); - await tester.tap(emailInputField); - await tester.pumpAndSettle(const Duration(seconds: 12)); - await findAndTapFAB(tester, logInButton); - - //Need to enter OTT manually and clicks the verify button automatically - final ottVerificationInputField = - find.byKey(const ValueKey("ottVerificationInputField")); - final verifyOttButton = find.byKey(const ValueKey("verifyOttButton")); - await tester.tap(ottVerificationInputField); - await tester.pumpAndSettle(const Duration(seconds: 6)); - await findAndTapFAB(tester, verifyOttButton); - - //Need to enter password manually and clicks the verify button automatically - final passwordInputField = - find.byKey(const ValueKey("passwordInputField")); - final verifyPasswordButton = - find.byKey(const ValueKey("verifyPasswordButton")); - await tester.tap(passwordInputField); - await tester.pumpAndSettle(const Duration(seconds: 10)); - await findAndTapFAB(tester, verifyPasswordButton); - - await tester.pumpAndSettle(const Duration(seconds: 1)); - await dismissUpdateAppDialog(tester); - - //Grant permission to access photos. Must manually click the system dialog. - final grantPermissionButton = - find.byKey(const ValueKey("grantPermissionButton")); - await tester.tap(grantPermissionButton); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - //Automatically skips backup - final skipBackupButton = find.byKey(const ValueKey("skipBackupButton")); - await tester.tap(skipBackupButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - await binding.traceAction( - () async { - //scroll gallery - final scrollablePositionedList = - find.byType(ScrollablePositionedList); - await tester.fling( - scrollablePositionedList, - const Offset(0, -5000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 5000), - 4500, - ); - - await tester.fling( - scrollablePositionedList, - const Offset(0, -7000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 7000), - 4500, - ); - - await tester.fling( - scrollablePositionedList, - const Offset(0, -9000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 9000), - 4500, - ); - await tester.pumpAndSettle(); - }, - reportKey: 'scrolling_summary', - ); - }); - }); -} - -Future findAndTapFAB(WidgetTester tester, Finder finder) async { - final RenderBox box = tester.renderObject(finder); - final Offset desiredOffset = Offset(box.size.width - 10, box.size.height / 2); - // Calculate the global position of the desired offset within the widget. - final Offset globalPosition = box.localToGlobal(desiredOffset); - await tester.tapAt(globalPosition); - await tester.pumpAndSettle(const Duration(seconds: 3)); -} - -Future dismissUpdateAppDialog(WidgetTester tester) async { - await tester.tapAt(const Offset(0, 0)); - await tester.pumpAndSettle(); -} diff --git a/mobile/integration_test/home_gallery_scroll_test.dart b/mobile/integration_test/home_gallery_scroll_test.dart new file mode 100644 index 000000000..3a87b6856 --- /dev/null +++ b/mobile/integration_test/home_gallery_scroll_test.dart @@ -0,0 +1,127 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:integration_test/integration_test.dart"; +import "package:logging/logging.dart"; +import "package:photos/main.dart" as app; +import "package:scrollable_positioned_list/scrollable_positioned_list.dart"; + +void main() { + group("Home gallery scroll test", () { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + testWidgets("Home gallery scroll test", semanticsEnabled: false, + (tester) async { + // https://github.com/flutter/flutter/issues/89749#issuecomment-1029965407 + tester.testTextInput.register(); + + await runZonedGuarded( + () async { + ///Ignore exceptions thrown by the app for the test to pass + WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (FlutterErrorDetails errorDetails) { + FlutterError.dumpErrorToConsole(errorDetails); + }; + + app.main(); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await dismissUpdateAppDialog(tester); + + final signInButton = find.byKey(const ValueKey("signInButton")); + await tester.tap(signInButton); + await tester.pumpAndSettle(); + + final emailInputField = find.byType(TextFormField); + final logInButton = find.byKey(const ValueKey("logInButton")); + //Fill email id here + await tester.enterText(emailInputField, "enter email here"); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(logInButton); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final passwordInputField = + find.byKey(const ValueKey("passwordInputField")); + final verifyPasswordButton = + find.byKey(const ValueKey("verifyPasswordButton")); + //Fill password here + await tester.enterText(passwordInputField, "enter password here"); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(verifyPasswordButton); + await tester.pumpAndSettle(); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + await dismissUpdateAppDialog(tester); + + //Grant permission to access photos. Must manually click the system dialog. + final grantPermissionButton = + find.byKey(const ValueKey("grantPermissionButton")); + await tester.tap(grantPermissionButton); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + //Automatically skips backup + final skipBackupButton = + find.byKey(const ValueKey("skipBackupButton")); + await tester.tap(skipBackupButton); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await binding.traceAction( + () async { + //scroll gallery + final scrollablePositionedList = + find.byType(ScrollablePositionedList); + await tester.fling( + scrollablePositionedList, + const Offset(0, -5000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 5000), + 4500, + ); + + await tester.fling( + scrollablePositionedList, + const Offset(0, -7000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 7000), + 4500, + ); + + await tester.fling( + scrollablePositionedList, + const Offset(0, -9000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 9000), + 4500, + ); + await tester.pumpAndSettle(); + }, + reportKey: 'home_gallery_scrolling_summary', + ); + }, + (error, stack) { + Logger("gallery_scroll_test").info(error, stack); + }, + ); + }); + }); +} + +Future dismissUpdateAppDialog(WidgetTester tester) async { + await tester.tapAt(const Offset(0, 0)); + await tester.pumpAndSettle(); +} diff --git a/mobile/lib/core/errors.dart b/mobile/lib/core/errors.dart index a7d616b92..d39f6f027 100644 --- a/mobile/lib/core/errors.dart +++ b/mobile/lib/core/errors.dart @@ -79,3 +79,5 @@ class LoginKeyDerivationError extends Error {} class SrpSetupNotCompleteError extends Error {} class SharingNotPermittedForFreeAccountsError extends Error {} + +class NoMediaLocationAccessError extends Error {} diff --git a/mobile/lib/db/file_updation_db.dart b/mobile/lib/db/file_updation_db.dart index 6496b71ed..426cc7a52 100644 --- a/mobile/lib/db/file_updation_db.dart +++ b/mobile/lib/db/file_updation_db.dart @@ -15,6 +15,7 @@ class FileUpdationDB { static const columnLocalID = 'local_id'; static const columnReason = 'reason'; static const livePhotoCheck = 'livePhotoCheck'; + static const androidMissingGPS = 'androidMissingGPS'; static const modificationTimeUpdated = 'modificationTimeUpdated'; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index ac3c273e0..7dbe50ec8 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1550,6 +1550,24 @@ class FilesDB { return result; } + Future> getLocalFilesBackedUpWithoutLocation(int userId) async { + final db = await instance.database; + final rows = await db.query( + filesTable, + columns: [columnLocalID], + distinct: true, + where: + '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' + 'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', + whereArgs: [userId], + ); + final result = []; + for (final row in rows) { + result.add(row[columnLocalID] as String); + } + return result; + } + // updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and // update the fileSize for the given uploadedFileID Future updateSizeForUploadIDs( diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 142ca37b7..5035838f4 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -20,11 +20,18 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'cs'; + static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), + "addCollaborators": m0, "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "addViewers": m1, "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( "Change location of selected items?"), "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), @@ -36,10 +43,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 964550e2e..5c9bd0ea5 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -21,27 +21,33 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'de'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Element hinzufügen', other: 'Elemente hinzufügen')}"; - static String m1(storageAmount, endDate) => + static String m3(storageAmount, endDate) => "Dein ${storageAmount} Add-on ist gültig bis ${endDate}"; - static String m2(emailOrName) => "Von ${emailOrName} hinzugefügt"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m4(emailOrName) => "Von ${emailOrName} hinzugefügt"; - static String m4(count) => + static String m5(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + + static String m6(count) => "${Intl.plural(count, zero: 'Keine Teilnehmer', one: '1 Teilnehmer', other: '${count} Teilnehmer')}"; - static String m5(versionValue) => "Version: ${versionValue}"; + static String m7(versionValue) => "Version: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Bitte kündigen Sie Ihr aktuelles Abo über ${paymentProvider} zuerst"; - static String m7(user) => + static String m9(user) => "Der Nutzer \"${user}\" wird keine weiteren Fotos zum Album hinzufügen können.\n\nJedoch kann er weiterhin vorhandene Bilder, welche durch ihn hinzugefügt worden sind, wieder entfernen"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Deine Familiengruppe hat bereits ${storageAmountInGb} GB erhalten', @@ -49,166 +55,163 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Du hast bereits ${storageAmountInGb} GB erhalten!', })}"; - static String m9(albumName) => + static String m11(albumName) => "Kollaborativer Link für ${albumName} erstellt"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Bitte kontaktiere ${familyAdminEmail} um dein Abo zu verwalten"; - static String m11(provider) => + static String m13(provider) => "Bitte kontaktieren Sie uns über support@ente.io, um Ihr ${provider} Abo zu verwalten."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Lösche ${count} Element', other: 'Lösche ${count} Elemente')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Lösche ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Der öffentliche Link zum Zugriff auf \"${albumName}\" wird entfernt."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Bitte sende eine E-Mail an ${supportEmail} von deiner registrierten E-Mail-Adresse"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "Du hast ${Intl.plural(count, one: '${count} duplizierte Datei', other: '${count} dupliziere Dateien')} gelöscht und (${storageSaved}!) freigegeben"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} Dateien, ${formattedSize} jede"; - static String m18(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; + static String m20(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} hat kein Ente-Konto.\n\nSenden Sie eine Einladung, um Fotos zu teilen."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} in diesem Album wurde(n) sicher gespeichert"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code für einen bezahlten Tarif anmeldet"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} kostenlos"; - static String m24(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; + static String m26(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; - static String m25(count) => + static String m27(count) => "Sie können immer noch ${Intl.plural(count, one: 'darauf', other: 'auf sie')} auf ente zugreifen, solange Sie ein aktives Abonnement haben"; - static String m26(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; + static String m28(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; - static String m27(count, formattedSize) => + static String m29(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 m28(currentlyProcessing, totalCount) => + static String m30(currentlyProcessing, totalCount) => "Verarbeite ${currentlyProcessing} / ${totalCount}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} Objekt', other: '${count} Objekte')}"; - static String m30(expiryTime) => "Link läuft am ${expiryTime} ab"; + static String m32(expiryTime) => "Link läuft am ${expiryTime} ab"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: 'keine Erinnerungsstücke', one: '${formattedCount} Erinnerung', other: '${formattedCount} Erinnerungsstücke')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Element verschieben', other: 'Elemente verschieben')}"; - static String m33(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m35(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Passwortstärke: ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Bitte kontaktiere den Support von ${providerName}, falls etwas abgebucht wurde"; - static String m36(reason) => - "Leider ist deine Zahlung aus folgendem Grund fehlgeschlagen: ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Kostenlose Testversion gültig bis ${endDate}.\nSie können anschließend ein bezahltes Paket auswählen."; - static String m38(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; + static String m39(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; - static String m39(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; + static String m40(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; - static String m40(storeName) => "Bewerte uns auf ${storeName}"; + static String m41(storeName) => "Bewerte uns auf ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "3. Ihr beide erhaltet ${storageInGB} GB* kostenlos"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefügte Fotos werden ebenfalls aus dem Album entfernt"; - static String m43(endDate) => "Erneuert am ${endDate}"; + static String m44(endDate) => "Erneuert am ${endDate}"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} Ergebnis gefunden', other: '${count} Ergebnisse gefunden')}"; - static String m45(count) => "${count} ausgewählt"; + static String m46(count) => "${count} ausgewählt"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} ausgewählt (${yourCount} von Ihnen)"; - static String m47(verificationID) => + static String m48(verificationID) => "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hey, kannst du bestätigen, dass dies deine ente.io Verifizierungs-ID ist: ${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(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 m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Teile mit bestimmten Personen', one: 'Teilen mit 1 Person', other: 'Teilen mit ${numberOfPeople} Personen')}"; - static String m51(emailIDs) => "Geteilt mit ${emailIDs}"; - - static String m52(fileType) => - "Dieses ${fileType} wird von deinem Gerät gelöscht."; + static String m52(emailIDs) => "Geteilt mit ${emailIDs}"; static String m53(fileType) => - "Dieses ${fileType} existiert auf ente.io und deinem Gerät."; + "Dieses ${fileType} wird von deinem Gerät gelöscht."; static String m54(fileType) => + "Dieses ${fileType} existiert auf ente.io und deinem Gerät."; + + static String m55(fileType) => "Dieses ${fileType} wird auf ente.io gelöscht."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} von ${totalAmount} ${totalStorageUnit} verwendet"; - static String m57(id) => + static String m58(id) => "Ihr ${id} ist bereits mit einem anderen \'ente\'-Konto verknüpft.\nWenn Sie Ihre ${id} mit diesem Konto verwenden möchten, kontaktieren Sie bitte unseren Support\'"; - static String m58(endDate) => "Ihr Abo endet am ${endDate}"; + static String m59(endDate) => "Ihr Abo endet am ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} Erinnerungsstücke gesichert"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "Diese erhalten auch ${storageAmountInGB} GB"; - static String m61(email) => "Dies ist ${email}s Verifizierungs-ID"; + static String m62(email) => "Dies ist ${email}s Verifizierungs-ID"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 Tag', other: '${count} Tage')}"; - static String m63(endDate) => "Gültig bis ${endDate}"; + static String m64(endDate) => "Gültig bis ${endDate}"; - static String m64(email) => "Verifiziere ${email}"; + static String m65(email) => "Verifiziere ${email}"; - static String m65(email) => + static String m66(email) => "Wir haben eine E-Mail an ${email} gesendet"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: 'vor einem Jahr', other: 'vor ${count} Jahren')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "Du hast ${storageSaved} erfolgreich freigegeben!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -229,16 +232,17 @@ class MessageLookup extends MessageLookupByLibrary { "Neue E-Mail-Adresse hinzufügen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Bearbeiter hinzufügen"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("Vom Gerät hinzufügen"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Ort hinzufügen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Hinzufügen"), "addMore": MessageLookupByLibrary.simpleMessage("Mehr hinzufügen"), "addNew": MessageLookupByLibrary.simpleMessage("Hinzufügen"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details der Add-ons"), - "addOnValidTill": m1, + "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Fotos hinzufügen"), "addSelected": @@ -249,11 +253,12 @@ class MessageLookup extends MessageLookupByLibrary { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Zum versteckten Album hinzufügen"), "addViewer": MessageLookupByLibrary.simpleMessage("Album teilen"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Füge deine Foto jetzt hinzu"), "addedAs": MessageLookupByLibrary.simpleMessage("Hinzugefügt als"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage( "Wird zu Favoriten hinzugefügt..."), "advanced": MessageLookupByLibrary.simpleMessage("Erweitert"), @@ -264,7 +269,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Nach 1 Woche"), "after1Year": MessageLookupByLibrary.simpleMessage("Nach 1 Jahr"), "albumOwner": MessageLookupByLibrary.simpleMessage("Besitzer"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Albumtitel"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album aktualisiert"), @@ -302,7 +307,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage( "Authentifizierung erforderlich"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Anwenden"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Code nutzen"), @@ -388,10 +393,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Du kannst nur Dateien entfernen, die dir gehören"), "cancel": MessageLookupByLibrary.simpleMessage("Abbrechen"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement kündigen"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Konnte geteilte Dateien nicht löschen"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -416,7 +421,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Freien Speicher einlösen"), "claimMore": MessageLookupByLibrary.simpleMessage("Mehr einlösen!"), "claimed": MessageLookupByLibrary.simpleMessage("Eingelöst"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Unkategorisiert leeren"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -441,7 +446,7 @@ class MessageLookup extends MessageLookupByLibrary { "Erstelle einen Link, um anderen zu ermöglichen, Fotos in deinem geteilten Album hinzuzufügen und zu sehen - ohne dass diese ein Konto von ente.io oder die App benötigen. Ideal, um Fotos von Events zu sammeln."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Gemeinschaftlicher Link"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Bearbeiter"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -468,10 +473,10 @@ class MessageLookup extends MessageLookupByLibrary { "Wiederherstellungsschlüssel bestätigen"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Bestätigen Sie ihren Wiederherstellungsschlüssel"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Support kontaktieren"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Kontakte"), "contents": MessageLookupByLibrary.simpleMessage("Inhalte"), "continueLabel": MessageLookupByLibrary.simpleMessage("Weiter"), @@ -549,11 +554,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Vom Gerät löschen"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Auf ente.io löschen"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Standort löschen"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Fotos löschen"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Es fehlt eine zentrale Funktion, die ich benötige"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -589,7 +594,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zuschauer können weiterhin Screenshots oder mit anderen externen Programmen Kopien der Bilder machen."), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Bitte beachten Sie:"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) deaktivieren"), "disablingTwofactorAuthentication": @@ -612,9 +617,9 @@ class MessageLookup extends MessageLookupByLibrary { "Herunterladen fehlgeschlagen"), "downloading": MessageLookupByLibrary.simpleMessage("Wird heruntergeladen..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), "editLocation": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), @@ -627,8 +632,8 @@ class MessageLookup extends MessageLookupByLibrary { "Änderungen des Standorts werden nur in ente sichtbar sein"), "eligible": MessageLookupByLibrary.simpleMessage("zulässig"), "email": MessageLookupByLibrary.simpleMessage("E-Mail"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-Mail-Verifizierung"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -728,8 +733,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dateitypen"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dateitypen und -namen"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dateien gelöscht"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( @@ -741,26 +746,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Passwort vergessen"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Kostenlos hinzugefügter Speicherplatz"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Freier Speicherplatz nutzbar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Kostenlose Testphase"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Gerätespeicher freiräumen"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Speicherplatz freigeben"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Bis zu 1000 Erinnerungsstücke angezeigt in der Galerie"), "general": MessageLookupByLibrary.simpleMessage("Allgemein"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generierung von Verschlüsselungscodes..."), - "genericProgress": m28, + "genericProgress": m30, "goToSettings": MessageLookupByLibrary.simpleMessage("Zu den Einstellungen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -823,7 +828,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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"), @@ -851,7 +856,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Geräte Limit"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiviert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Abgelaufen"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Ablaufdatum des Links"), "linkHasExpired": @@ -901,6 +906,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Ausloggen"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Dies wird über Logs gesendet, um uns zu helfen, Ihr Problem zu beheben. Bitte beachten Sie, dass Dateinamen aufgenommen werden, um Probleme mit bestimmten Dateien zu beheben."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Drücken Sie lange auf ein Element, um es im Vollbildmodus anzuzeigen"), "lostDevice": MessageLookupByLibrary.simpleMessage("Gerät verloren?"), @@ -922,7 +930,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Karten"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobil, Web, Desktop"), @@ -932,12 +940,12 @@ class MessageLookup extends MessageLookupByLibrary { "Ändere deine Suchanfrage oder suche nach"), "moments": MessageLookupByLibrary.simpleMessage("Momente"), "monthly": MessageLookupByLibrary.simpleMessage("Monatlich"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Zum Album verschieben"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Zu verstecktem Album verschieben"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage( "In den Papierkorb verschoben"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1010,15 +1018,14 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Passwort erfolgreich geändert"), "passwordLock": MessageLookupByLibrary.simpleMessage("Passwort Sperre"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Wir speichern dieses Passwort nicht. Wenn du es vergisst, können wir deine Daten nicht entschlüsseln"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Zahlungsdetails"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Zahlung fehlgeschlagen"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("Ausstehende Elemente"), "pendingSync": @@ -1045,7 +1052,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Album anheften"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Album auf dem Fernseher wiedergeben"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore Abo"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1057,12 +1064,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Bitte erteile die nötigen Berechtigungen"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Bitte logge dich erneut ein"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bitte versuche es erneut"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1098,7 +1105,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Ticket erstellen"), "rateTheApp": MessageLookupByLibrary.simpleMessage("App bewerten"), "rateUs": MessageLookupByLibrary.simpleMessage("Bewerte uns"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Wiederherstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), @@ -1131,7 +1138,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gib diesen Code an deine Freunde"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Sie schließen ein bezahltes Abo ab"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Weiterempfehlungen"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Einlösungen sind derzeit pausiert"), @@ -1157,7 +1164,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Link entfernen"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Öffentlichen Link entfernen"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1171,7 +1178,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Datei umbenennen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement erneuern"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "resendEmail": @@ -1234,7 +1241,7 @@ class MessageLookup extends MessageLookupByLibrary { "Gruppiere Fotos, die innerhalb des Radius eines bestimmten Fotos aufgenommen wurden"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), "selectALocation": MessageLookupByLibrary.simpleMessage("Standort auswählen"), @@ -1261,8 +1268,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden aus allen Alben gelöscht und in den Papierkorb verschoben."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Absenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-Mail senden"), "sendInvite": MessageLookupByLibrary.simpleMessage("Einladung senden"), @@ -1285,16 +1292,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Teile jetzt ein Album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link teilen"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Teile mit ausgewählten Personen"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Lade ente herunter, damit wir einfach Fotos und Videos in höchster Qualität teilen können\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Mit Nicht-Ente-Benutzern teilen"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Teile dein erstes Album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1305,7 +1312,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": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Mit mir geteilt"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Mit dir geteilt"), @@ -1320,11 +1327,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Andere Geräte abmelden"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ich stimme den Nutzungsbedingungen und der Datenschutzerklärung zu"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Es wird aus allen Alben gelöscht."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Überspringen"), "social": MessageLookupByLibrary.simpleMessage("Social Media"), "someItemsAreInBothEnteAndYourDevice": @@ -1366,13 +1373,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Speicherplatz"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sie"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Speichergrenze überschritten"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Stark"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Abonnieren"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Sieht aus, als sei dein Abonnement abgelaufen. Bitte abonniere, um das Teilen zu aktivieren."), @@ -1389,7 +1396,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Verbesserung vorschlagen"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisierung angehalten"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronisiere …"), @@ -1418,7 +1425,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Diese Elemente werden von deinem Gerät gelöscht."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Sie werden aus allen Alben gelöscht."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1434,7 +1441,7 @@ class MessageLookup extends MessageLookupByLibrary { "Diese E-Mail-Adresse wird bereits verwendet"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Dieses Bild hat keine Exif-Daten"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dies ist deine Verifizierungs-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1451,7 +1458,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("Gesamt"), "totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"), "trash": MessageLookupByLibrary.simpleMessage("Papierkorb"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Erneut versuchen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Aktiviere die Sicherung, um automatisch neu hinzugefügte Dateien dieses Ordners auf ente hochzuladen."), @@ -1506,7 +1513,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ausgewähltes Foto verwenden"), "usedSpace": MessageLookupByLibrary.simpleMessage("Belegter Speicherplatz"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut"), @@ -1515,7 +1522,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verifizieren"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Passkey verifizieren"), @@ -1548,12 +1555,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Wir unterstützen keine Bearbeitung von Fotos und Alben, die du noch nicht besitzt"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Schwach"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), "yearly": MessageLookupByLibrary.simpleMessage("Jährlich"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, kündigen"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1583,7 +1590,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kannst nicht mit dir selbst teilen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du hast keine archivierten Elemente."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Dein Benutzerkonto wurde gelöscht"), "yourMap": MessageLookupByLibrary.simpleMessage("Deine Karte"), diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 5f8a1cf33..7b0f06197 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -21,191 +21,194 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Add item', other: 'Add items')}"; - static String m1(storageAmount, endDate) => + static String m3(storageAmount, endDate) => "Your ${storageAmount} add-on is valid till ${endDate}"; - static String m2(emailOrName) => "Added by ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Added successfully to ${albumName}"; + static String m4(emailOrName) => "Added by ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Added successfully to ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'No Participants', one: '1 Participant', other: '${count} Participants')}"; - static String m5(versionValue) => "Version: ${versionValue}"; + static String m7(versionValue) => "Version: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Please cancel your existing subscription from ${paymentProvider} first"; - static String m7(user) => + static String m9(user) => "${user} will not be able to add more photos to this album\n\nThey will still be able to remove existing photos added by them"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Your family has claimed ${storageAmountInGb} GB so far', 'false': 'You have claimed ${storageAmountInGb} GB so far', 'other': 'You have claimed ${storageAmountInGb} GB so far!', })}"; - static String m9(albumName) => "Collaborative link created for ${albumName}"; + static String m11(albumName) => "Collaborative link created for ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Please contact ${familyAdminEmail} to manage your subscription"; - static String m11(provider) => + static String m13(provider) => "Please contact us at support@ente.io to manage your ${provider} subscription."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Deleting ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "This will remove the public link for accessing \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Please drop an email to ${supportEmail} from your registered email address"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "You have cleaned up ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, saving (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} files, ${formattedSize} each"; - static String m18(newEmail) => "Email changed to ${newEmail}"; + static String m20(newEmail) => "Email changed to ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} does not have an ente account.\n\nSend them an invite to share photos."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} in this album has been backed up safely"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB each time someone signs up for a paid plan and applies your code"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} free"; - static String m24(endDate) => "Free trial valid till ${endDate}"; + static String m26(endDate) => "Free trial valid till ${endDate}"; - static String m25(count) => + static String m27(count) => "You can still access ${Intl.plural(count, one: 'it', other: 'them')} on ente as long as you have an active subscription"; - static String m26(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(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 m28(currentlyProcessing, totalCount) => + static String m30(currentlyProcessing, totalCount) => "Processing ${currentlyProcessing} / ${totalCount}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m30(expiryTime) => "Link will expire on ${expiryTime}"; + static String m32(expiryTime) => "Link will expire on ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: 'no memories', one: '${formattedCount} memory', other: '${formattedCount} memories')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Move item', other: 'Move items')}"; - static String m33(albumName) => "Moved successfully to ${albumName}"; + static String m35(albumName) => "Moved successfully to ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Password strength: ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Please talk to ${providerName} support if you were charged"; - static String m36(reason) => - "Unfortunately your payment failed due to ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards."; - static String m38(toEmail) => "Please email us at ${toEmail}"; + static String m39(toEmail) => "Please email us at ${toEmail}"; - static String m39(toEmail) => "Please send the logs to \n${toEmail}"; + static String m40(toEmail) => "Please send the logs to \n${toEmail}"; - static String m40(storeName) => "Rate us on ${storeName}"; + static String m41(storeName) => "Rate us on ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "3. Both of you get ${storageInGB} GB* free"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} will be removed from this shared album\n\nAny photos added by them will also be removed from the album"; - static String m43(endDate) => "Subscription renews on ${endDate}"; + static String m44(endDate) => "Subscription renews on ${endDate}"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} result found', other: '${count} results found')}"; - static String m45(count) => "${count} selected"; + static String m46(count) => "${count} selected"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} selected (${yourCount} yours)"; - static String m47(verificationID) => + static String m48(verificationID) => "Here\'s my verification ID: ${verificationID} for ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hey, can you confirm that this is your ente.io verification ID: ${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(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 m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Share with specific people', one: 'Shared with 1 person', other: 'Shared with ${numberOfPeople} people')}"; - static String m51(emailIDs) => "Shared with ${emailIDs}"; - - static String m52(fileType) => - "This ${fileType} will be deleted from your device."; + static String m52(emailIDs) => "Shared with ${emailIDs}"; static String m53(fileType) => + "This ${fileType} will be deleted from your device."; + + static String m54(fileType) => "This ${fileType} is in both ente and your device."; - static String m54(fileType) => "This ${fileType} will be deleted from ente."; + static String m55(fileType) => "This ${fileType} will be deleted from ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} of ${totalAmount} ${totalStorageUnit} used"; - static String m57(id) => + static String m58(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 m58(endDate) => + static String m59(endDate) => "Your subscription will be cancelled on ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} memories preserved"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "They also get ${storageAmountInGB} GB"; - static String m61(email) => "This is ${email}\'s Verification ID"; + static String m62(email) => "This is ${email}\'s Verification ID"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 day', other: '${count} days')}"; - static String m63(endDate) => "Valid till ${endDate}"; + static String m64(endDate) => "Valid till ${endDate}"; - static String m64(email) => "Verify ${email}"; + static String m65(email) => "Verify ${email}"; - static String m65(email) => "We have sent a mail to ${email}"; + static String m66(email) => "We have sent a mail to ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} year ago', other: '${count} years ago')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "You have successfully freed up ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -224,16 +227,17 @@ class MessageLookup extends MessageLookupByLibrary { "addANewEmail": MessageLookupByLibrary.simpleMessage("Add a new email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Add collaborator"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("Add from device"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Add location"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Add"), "addMore": MessageLookupByLibrary.simpleMessage("Add more"), "addNew": MessageLookupByLibrary.simpleMessage("Add new"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details of add-ons"), - "addOnValidTill": m1, + "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Add photos"), "addSelected": MessageLookupByLibrary.simpleMessage("Add selected"), @@ -242,11 +246,12 @@ class MessageLookup extends MessageLookupByLibrary { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), "addViewer": MessageLookupByLibrary.simpleMessage("Add viewer"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Add your photos now"), "addedAs": MessageLookupByLibrary.simpleMessage("Added as"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Adding to favorites..."), "advanced": MessageLookupByLibrary.simpleMessage("Advanced"), @@ -257,7 +262,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("After 1 week"), "after1Year": MessageLookupByLibrary.simpleMessage("After 1 year"), "albumOwner": MessageLookupByLibrary.simpleMessage("Owner"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Album title"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album updated"), "albums": MessageLookupByLibrary.simpleMessage("Albums"), @@ -291,7 +296,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Authentication required"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Apply"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Apply code"), @@ -375,10 +380,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Can only remove files owned by you"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancel subscription"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("Cannot delete shared files"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -402,7 +407,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim free storage"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim more!"), "claimed": MessageLookupByLibrary.simpleMessage("Claimed"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Clean Uncategorized"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -427,7 +432,7 @@ class MessageLookup extends MessageLookupByLibrary { "Create a link to allow people to add and view photos in your shared album without needing an ente app or account. Great for collecting event photos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Collaborative link"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborator"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -454,10 +459,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Confirm recovery key"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("Confirm your recovery key"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contact support"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contents"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continue"), @@ -532,11 +537,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Delete from device"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Delete from ente"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Delete location"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "It’s missing a key feature that I need"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -573,7 +578,7 @@ class MessageLookup extends MessageLookupByLibrary { "Viewers can still take screenshots or save a copy of your photos using external tools"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Please note"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Disable two-factor"), "disablingTwofactorAuthentication": @@ -594,9 +599,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Download failed"), "downloading": MessageLookupByLibrary.simpleMessage("Downloading..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -607,8 +612,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("eligible"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Email verification"), "emailYourLogs": @@ -703,8 +708,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("File types and names"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( "Find people quickly by searching by name"), @@ -715,24 +720,24 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Forgot password"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Free storage claimed"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Free storage usable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Free trial"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Free up device space"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Free up space"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Up to 1000 memories shown in gallery"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generating encryption keys..."), - "genericProgress": m28, + "genericProgress": m30, "goToSettings": MessageLookupByLibrary.simpleMessage("Go to settings"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -745,6 +750,7 @@ class MessageLookup extends MessageLookupByLibrary { "We don\'t track app installs. It\'d help if you told us where you found us!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "How did you hear about Ente? (optional)"), + "help": MessageLookupByLibrary.simpleMessage("Help"), "hidden": MessageLookupByLibrary.simpleMessage("Hidden"), "hide": MessageLookupByLibrary.simpleMessage("Hide"), "hiding": MessageLookupByLibrary.simpleMessage("Hiding..."), @@ -790,7 +796,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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Items show the number of days remaining before permanent deletion"), @@ -815,7 +821,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Device limit"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Enabled"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expired"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expiry"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link has expired"), @@ -864,6 +870,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Logout"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Long-press on an item to view in full-screen"), @@ -885,7 +894,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Maps"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), @@ -895,11 +904,11 @@ class MessageLookup extends MessageLookupByLibrary { "Modify your query, or try searching for"), "moments": MessageLookupByLibrary.simpleMessage("Moments"), "monthly": MessageLookupByLibrary.simpleMessage("Monthly"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Move to album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Moved to trash"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Moving files to album..."), @@ -968,14 +977,15 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Password changed successfully"), "passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "We don\'t store this password, so if you forget, we cannot decrypt your data"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Payment details"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Payment failed"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "Unfortunately your payment failed. Please contact support and we\'ll help you out!"), + "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"), "pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"), "people": MessageLookupByLibrary.simpleMessage("People"), @@ -1000,7 +1010,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pick center point"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"), "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1012,12 +1022,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Please contact support if the problem persists"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Please grant permissions"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Please login again"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Please try again"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1052,7 +1062,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Raise ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Rate the app"), "rateUs": MessageLookupByLibrary.simpleMessage("Rate us"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recover"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recover account"), @@ -1083,7 +1093,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Give this code to your friends"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. They sign up for a paid plan"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referrals"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referrals are currently paused"), @@ -1107,7 +1117,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remove link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remove participant"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Remove public link"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1121,7 +1131,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rename file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renew subscription"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Report a bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Report bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Resend email"), @@ -1181,7 +1191,7 @@ class MessageLookup extends MessageLookupByLibrary { "Group photos that are taken within some radius of a photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Invite people, and you\'ll see all photos shared by them here"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Security"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), @@ -1208,8 +1218,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Selected items will be deleted from all albums and moved to trash."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"), @@ -1231,16 +1241,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Share an album now"), "shareLink": MessageLookupByLibrary.simpleMessage("Share link"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Share only with the people you want"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download ente so we can easily share original quality photos and videos\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Share with non-ente users"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Share your first album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1251,7 +1261,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": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Shared with you"), @@ -1265,11 +1275,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sign out other devices"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "I agree to the terms of service and privacy policy"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "It will be deleted from all albums."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Skip"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1307,13 +1317,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Storage limit exceeded"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Strong"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Looks like your subscription has expired. Please subscribe to enable sharing."), @@ -1330,7 +1340,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggest features"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sync stopped"), "syncing": MessageLookupByLibrary.simpleMessage("Syncing..."), "systemTheme": MessageLookupByLibrary.simpleMessage("System"), @@ -1356,7 +1366,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "These items will be deleted from your device."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "They will be deleted from all albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1372,7 +1382,7 @@ class MessageLookup extends MessageLookupByLibrary { "This email is already in use"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("This image has no exif data"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "This is your Verification ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1388,7 +1398,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Total size"), "trash": MessageLookupByLibrary.simpleMessage("Trash"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Try again"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Turn on backup to automatically upload files added to this device folder to ente."), @@ -1440,7 +1450,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Use selected photo"), "usedSpace": MessageLookupByLibrary.simpleMessage("Used space"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verification failed, please try again"), @@ -1448,7 +1458,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verification ID"), "verify": MessageLookupByLibrary.simpleMessage("Verify"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": @@ -1479,11 +1489,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We don\'t support editing photos and albums that you don\'t own yet"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Weak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welcome back!"), "yearly": MessageLookupByLibrary.simpleMessage("Yearly"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "yesCancel": MessageLookupByLibrary.simpleMessage("Yes, cancel"), "yesConvertToViewer": @@ -1513,7 +1523,7 @@ class MessageLookup extends MessageLookupByLibrary { "You cannot share with yourself"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "You don\'t have any archived items."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "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 24da1b739..8ceb97ad3 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -21,24 +21,30 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'es'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Agregar elemento', other: 'Agregar elementos')}}"; - static String m2(emailOrName) => "Añadido por ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Añadido exitosamente a ${albumName}"; + static String m4(emailOrName) => "Añadido por ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Añadido exitosamente a ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'No hay Participantes', one: '1 Participante', other: '${count} Participantes')}"; - static String m5(versionValue) => "Versión: ${versionValue}"; + static String m7(versionValue) => "Versión: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Por favor, cancele primero su suscripción existente de ${paymentProvider}"; - static String m7(user) => + static String m9(user) => "${user} no podrá añadir más fotos a este álbum\n\nTodavía podrán eliminar las fotos ya añadidas por ellos"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Su familia ha reclamado ${storageAmountInGb} GB hasta el momento', @@ -48,145 +54,143 @@ class MessageLookup extends MessageLookupByLibrary { '¡Tú has reclamado ${storageAmountInGb} GB hasta el momento!', })}"; - static String m9(albumName) => "Enlace colaborativo creado para ${albumName}"; + static String m11(albumName) => + "Enlace colaborativo creado para ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Por favor contacta con ${familyAdminEmail} para administrar tu suscripción"; - static String m11(provider) => + static String m13(provider) => "Por favor, contáctenos en support@ente.io para gestionar su suscripción a ${provider}."; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Borrando ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Esto eliminará el enlace público para acceder a \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Por favor, envíe un email a ${supportEmail} desde su dirección de correo electrónico registrada"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "¡Has limpiado ${Intl.plural(count, one: '${count} archivo duplicado', other: '${count} archivos duplicados')}, ahorrando (${storageSaved}!)"; - static String m18(newEmail) => "Correo cambiado a ${newEmail}"; + static String m20(newEmail) => "Correo cambiado a ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} no tiene una cuenta ente.\n\nEnvíale una invitación para compartir fotos."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} en este dispositivo han sido respaldados de forma segura"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} en este álbum ha sido respaldado de forma segura"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguien se registra en un plan de pago y aplica tu código"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} gratis"; - static String m24(endDate) => "Prueba gratuita válida hasta${endDate}"; + static String m26(endDate) => "Prueba gratuita válida hasta${endDate}"; - static String m25(count) => + static String m27(count) => "Aún puedes acceder ${Intl.plural(count, one: 'si', other: 'entonces')} en ente mientras mantengas una suscripción activa"; - static String m26(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(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 m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementos')}"; - static String m30(expiryTime) => "El enlace caducará en ${expiryTime}"; + static String m32(expiryTime) => "El enlace caducará en ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: 'no recuerdos', one: '${formattedCount} recuerdo', other: '${formattedCount} recuerdos')}\n"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Mover elemento', other: 'Mover elementos')}"; - static String m33(albumName) => "Movido exitosamente a ${albumName}"; + static String m35(albumName) => "Movido exitosamente a ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Seguridad de la contraseña : ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Por favor hable con el soporte de ${providerName} si se le cobró"; - static String m36(reason) => - "Lamentablemente tu pago falló debido a ${reason}"; - - static String m38(toEmail) => + static String m39(toEmail) => "Por favor, envíanos un correo electrónico a ${toEmail}"; - static String m39(toEmail) => "Por favor, envíe los registros a ${toEmail}"; + static String m40(toEmail) => "Por favor, envíe los registros a ${toEmail}"; - static String m40(storeName) => "Califícanos en ${storeName}"; + static String m41(storeName) => "Califícanos en ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "3. Ambos obtienen ${storageInGB} GB* gratis"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} será eliminado de este álbum compartido\n\nCualquier foto añadida por ellos también será eliminada del álbum"; - static String m43(endDate) => "Se renueva el ${endDate}"; + static String m44(endDate) => "Se renueva el ${endDate}"; - static String m45(count) => "${count} seleccionados"; + static String m46(count) => "${count} seleccionados"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} seleccionados (${yourCount} tuyos)"; - static String m47(verificationID) => + static String m48(verificationID) => "Aquí está mi ID de verificación: ${verificationID} para ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hola, ¿puedes confirmar que esta es tu ID de verificación ente.io: ${verificationID}?"; - static String m49(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente código de referencia: ${referralCode} \n\nAplicarlo en Ajustes → General → Referencias para obtener ${referralStorageInGB} GB gratis después de registrarse en un plan de pago\n\nhttps://ente.io"; - static String m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartir con personas específicas', one: 'Compartido con 1 persona', other: 'Compartido con ${numberOfPeople} personas')}"; - static String m51(emailIDs) => "Compartido con ${emailIDs}"; - - static String m52(fileType) => - "Este ${fileType} se eliminará de tu dispositivo."; + static String m52(emailIDs) => "Compartido con ${emailIDs}"; static String m53(fileType) => + "Este ${fileType} se eliminará de tu dispositivo."; + + static String m54(fileType) => "Este ${fileType} está tanto en ente como en tu dispositivo."; - static String m54(fileType) => "Este ${fileType} se eliminará de ente."; + static String m55(fileType) => "Este ${fileType} se eliminará de ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usados"; - static String m57(id) => + static String m58(id) => "Su ${id} ya está vinculado a otra cuenta ente.\nSi desea utilizar su ${id} con esta cuenta, póngase en contacto con nuestro servicio de asistencia\'\'"; - static String m58(endDate) => "Tu suscripción se cancelará el ${endDate}"; + static String m59(endDate) => "Tu suscripción se cancelará el ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} recuerdos conservados"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "También obtienen ${storageAmountInGB} GB"; - static String m61(email) => "Este es el ID de verificación de ${email}"; + static String m62(email) => "Este es el ID de verificación de ${email}"; - static String m64(email) => "Verificar ${email}"; + static String m65(email) => "Verificar ${email}"; - static String m65(email) => + static String m66(email) => "Hemos enviado un correo a ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} hace un año', other: '${count} hace años')}"; - static String m67(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; + static String m68(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -200,12 +204,12 @@ class MessageLookup extends MessageLookupByLibrary { "Entiendo que si pierdo mi contraseña podría perder mis datos, ya que mis datos están cifrados de extremo a extremo."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sesiónes activas"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage( "Agregar nuevo correo electrónico"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Agregar colaborador"), - "addItem": m0, + "addCollaborators": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Agregar ubicación"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Añadir"), @@ -215,9 +219,10 @@ class MessageLookup extends MessageLookupByLibrary { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), "addViewer": MessageLookupByLibrary.simpleMessage("Añadir espectador"), + "addViewers": m1, "addedAs": MessageLookupByLibrary.simpleMessage("Agregado como"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Añadiendo a favoritos..."), "advanced": MessageLookupByLibrary.simpleMessage("Avanzado"), @@ -230,7 +235,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Después de una semana"), "after1Year": MessageLookupByLibrary.simpleMessage("Después de un año"), "albumOwner": MessageLookupByLibrary.simpleMessage("Propietario"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Título del álbum"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Álbum actualizado"), @@ -248,7 +253,7 @@ class MessageLookup extends MessageLookupByLibrary { "Permitir que la gente añada fotos"), "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage( "Android, iOS, Web, Computadora"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("ID de Apple"), "apply": MessageLookupByLibrary.simpleMessage("Aplicar"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Usar código"), @@ -330,10 +335,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Sólo puede eliminar archivos de tu propiedad"), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar suscripción"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "centerPoint": MessageLookupByLibrary.simpleMessage("Punto central"), "changeEmail": MessageLookupByLibrary.simpleMessage("Cambiar correo electrónico"), @@ -354,7 +359,7 @@ class MessageLookup extends MessageLookupByLibrary { "Reclamar almacenamiento gratis"), "claimMore": MessageLookupByLibrary.simpleMessage("¡Reclama más!"), "claimed": MessageLookupByLibrary.simpleMessage("Reclamado"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "clearCaches": MessageLookupByLibrary.simpleMessage("Limpiar caché"), "click": MessageLookupByLibrary.simpleMessage("• Click"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( @@ -372,7 +377,7 @@ class MessageLookup extends MessageLookupByLibrary { "Crea un enlace para que la gente pueda añadir y ver fotos en tu álbum compartido sin necesidad de la aplicación ente o una cuenta. Genial para recolectar fotos de eventos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Enlace colaborativo"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -397,10 +402,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmar clave de recuperación"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirme su clave de recuperación"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contactar con soporte"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( @@ -473,7 +478,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Borrar la ubicación"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Borrar las fotos"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Falta una característica clave que necesito"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -507,7 +512,7 @@ class MessageLookup extends MessageLookupByLibrary { "Los espectadores todavía pueden tomar capturas de pantalla o guardar una copia de sus fotos usando herramientas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Por favor tenga en cuenta"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Deshabilitar dos factores"), "disablingTwofactorAuthentication": @@ -528,8 +533,8 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Descarga fallida"), "downloading": MessageLookupByLibrary.simpleMessage("Descargando..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -541,8 +546,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegible"), "email": MessageLookupByLibrary.simpleMessage("Correo electrónico"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Envíe sus registros por correo electrónico"), "empty": MessageLookupByLibrary.simpleMessage("Vaciar"), @@ -631,12 +636,10 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage( "Archivo guardado en la galería"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Archivos eliminados"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("Voltear"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para tus recuerdos"), @@ -644,18 +647,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Olvidé mi contraseña"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Almacenamiento gratuito reclamado"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Almacenamiento libre disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espacio del dispositivo"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espacio"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Hasta 1000 memorias mostradas en la galería"), "general": MessageLookupByLibrary.simpleMessage("General"), @@ -701,7 +704,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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Los artículos muestran el número de días restantes antes de ser borrados permanente"), @@ -730,7 +733,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Límite del dispositivo"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Habilitado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Vencido"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Enlace vence"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("El enlace ha caducado"), @@ -779,6 +782,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Cerrar sesión"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Esto enviará registros para ayudarnos a depurar su problema. Tenga en cuenta que los nombres de los archivos se incluirán para ayudar a rastrear problemas con archivos específicos."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Pulsación prolongada en un elemento para ver en pantalla completa"), "lostDevice": @@ -796,7 +802,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Administrar tu suscripción"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Mercancías"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Celular, Web, Computadora"), @@ -805,11 +811,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), "monthly": MessageLookupByLibrary.simpleMessage("Mensual"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido a la papelera"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -859,14 +865,13 @@ class MessageLookup extends MessageLookupByLibrary { "Contraseña cambiada correctamente"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueo por contraseña"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "No almacenamos esta contraseña, así que si la olvidas, no podemos descifrar tus datos"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Detalles de pago"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Pago fallido"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedTalkToProvider": m37, "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronización pendiente"), "peopleUsingYourCode": @@ -893,12 +898,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor contacte a soporte técnico si el problema persiste"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Por favor, concede permiso"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, vuelva a iniciar sesión"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inténtalo nuevamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -932,7 +937,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evalúa la aplicación"), "rateUs": MessageLookupByLibrary.simpleMessage("Califícanos"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), @@ -964,7 +969,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dale este código a tus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Se inscriben a un plan pagado"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referidos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Las referencias están actualmente en pausa"), @@ -989,7 +994,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminar enlace"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Quitar participante"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Quitar enlace público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1003,7 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renombrar archivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar suscripción"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar un error"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar error"), "resendEmail": @@ -1065,8 +1070,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Los archivos seleccionados serán eliminados de todos los álbumes y movidos a la papelera."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar correo electrónico"), @@ -1091,32 +1096,32 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartir un álbum ahora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartir enlace"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Comparte sólo con la gente que quieres"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarga ente para que podamos compartir fácilmente fotos y videos en su calidad original\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartir con usuarios no ente"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Comparte tu primer álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Crear álbumes compartidos y colaborativos con otros usuarios ente, incluyendo usuarios en planes gratuitos."), "sharedByMe": MessageLookupByLibrary.simpleMessage("Compartido por mí"), - "sharedWith": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartido conmigo"), "sharing": MessageLookupByLibrary.simpleMessage("Compartiendo..."), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Estoy de acuerdo con los términos del servicio y la política de privacidad"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Se borrará de todos los álbumes."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Omitir"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1151,13 +1156,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Almacenamiento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Usted"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Límite de datos excedido"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Segura"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Suscribirse"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Parece que su suscripción ha caducado. Por favor, suscríbase para habilitar el compartir."), @@ -1170,7 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir una característica"), "support": MessageLookupByLibrary.simpleMessage("Soporte"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronización detenida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1198,7 +1203,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estos elementos se eliminarán de tu dispositivo."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Se borrarán de todos los álbumes."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1214,7 +1219,7 @@ class MessageLookup extends MessageLookupByLibrary { "Este correo electrónico ya está en uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagen no tiene datos exif"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Esta es tu ID de verificación"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1284,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Verificar correo electrónico"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Verificar contraseña"), @@ -1307,12 +1312,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "No admitimos la edición de fotos y álbunes que aún no son tuyos"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Poco segura"), "welcomeBack": MessageLookupByLibrary.simpleMessage("¡Bienvenido de nuevo!"), "yearly": MessageLookupByLibrary.simpleMessage("Anualmente"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Sí"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sí, cancelar"), "yesConvertToViewer": @@ -1342,7 +1347,7 @@ class MessageLookup extends MessageLookupByLibrary { "No puedes compartir contigo mismo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "No tienes nada de elementos archivados."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Su cuenta ha sido eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index dc4f98965..035d6c232 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -21,24 +21,30 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'fr'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Ajoutez un objet', other: 'Ajoutez des objets')}"; - static String m2(emailOrName) => "Ajouté par ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Ajouté avec succès à ${albumName}"; + static String m4(emailOrName) => "Ajouté par ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Ajouté avec succès à ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'Aucun Participant', one: '1 Participant', other: '${count} Participants')}"; - static String m5(versionValue) => "Version : ${versionValue}"; + static String m7(versionValue) => "Version : ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Veuillez d\'abord annuler votre abonnement existant de ${paymentProvider}"; - static String m7(user) => + static String m9(user) => "${user} ne pourra pas ajouter plus de photos à cet album\n\nIl pourrait toujours supprimer les photos existantes ajoutées par eux"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Votre famille a demandé ${storageAmountInGb} GB jusqu\'à présent', @@ -48,161 +54,158 @@ class MessageLookup extends MessageLookupByLibrary { 'Vous avez réclamé ${storageAmountInGb} GB jusqu\'à présent!', })}"; - static String m9(albumName) => "Lien collaboratif créé pour ${albumName}"; + static String m11(albumName) => "Lien collaboratif créé pour ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour gérer votre abonnement"; - static String m11(provider) => + static String m13(provider) => "Veuillez nous contacter à support@ente.io pour gérer votre abonnement ${provider}."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Supprimer le fichier', other: 'Supprimer ${count} fichiers')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Suppression de ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Cela supprimera le lien public pour accéder à \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Veuillez envoyer un e-mail à ${supportEmail} depuis votre adresse enregistrée"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "Vous avez nettoyé ${Intl.plural(count, one: '${count} fichier dupliqué', other: '${count} fichiers dupliqués')}, sauvegarde (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} fichiers, ${formattedSize} chacun"; - static String m18(newEmail) => "L\'e-mail a été changé en ${newEmail}"; + static String m20(newEmail) => "L\'e-mail a été changé en ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} n\'a pas de compte ente.\n\nEnvoyez une invitation pour partager des photos."; - static String m20(count, formattedNumber) => + static String m22(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 m21(count, formattedNumber) => + static String m23(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 m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} Go chaque fois que quelqu\'un s\'inscrit à une offre payante et applique votre code"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} libre"; - static String m24(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; + static String m26(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; - static String m25(count) => + static String m27(count) => "Vous pouvez toujours ${Intl.plural(count, one: 'y', other: 'y')} accéder sur ente tant que vous avez un abonnement actif"; - static String m26(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(count, formattedSize) => "${Intl.plural(count, one: 'Peut être supprimé de l\'appareil pour libérer ${formattedSize}', other: 'Peuvent être supprimés de l\'appareil pour libérer ${formattedSize}')}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} objet', other: '${count} objets')}"; - static String m30(expiryTime) => "Le lien expirera le ${expiryTime}"; + static String m32(expiryTime) => "Le lien expirera le ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} mémoire', other: '${formattedCount} souvenirs')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Déplacez l\'objet', other: 'Déplacez des objets')}"; - static String m33(albumName) => "Déplacé avec succès vers ${albumName}"; + static String m35(albumName) => "Déplacé avec succès vers ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Sécurité du mot de passe : ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Veuillez contacter le support ${providerName} si vous avez été facturé"; - static String m36(reason) => - "Malheureusement, votre paiement a échoué pour ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Essai gratuit valable jusqu\'à ${endDate}.\nVous pouvez choisir un plan payant par la suite."; - static String m38(toEmail) => "Merci de nous envoyer un e-mail à ${toEmail}"; + static String m39(toEmail) => "Merci de nous envoyer un e-mail à ${toEmail}"; - static String m39(toEmail) => "Envoyez les logs à ${toEmail}"; + static String m40(toEmail) => "Envoyez les logs à ${toEmail}"; - static String m40(storeName) => "Notez-nous sur ${storeName}"; + static String m41(storeName) => "Notez-nous sur ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "3. Vous recevez tous les deux ${storageInGB} GB* gratuits"; - static String m42(userEmail) => + static String m43(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 m43(endDate) => "Renouvellement le ${endDate}"; + static String m44(endDate) => "Renouvellement le ${endDate}"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}"; - static String m45(count) => "${count} sélectionné(s)"; + static String m46(count) => "${count} sélectionné(s)"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} sélectionné(s) (${yourCount} à vous)"; - static String m47(verificationID) => + static String m48(verificationID) => "Voici mon ID de vérification : ${verificationID} pour ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hé, pouvez-vous confirmer qu\'il s\'agit de votre ID de vérification ente.io : ${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "code de parrainage ente : ${referralCode} \n\nAppliquez 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 m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partagez avec des personnes spécifiques', one: 'Partagé avec 1 personne', other: 'Partagé avec ${numberOfPeople} des gens')}"; - static String m51(emailIDs) => "Partagé avec ${emailIDs}"; - - static String m52(fileType) => - "Elle ${fileType} sera supprimée de votre appareil."; + static String m52(emailIDs) => "Partagé avec ${emailIDs}"; static String m53(fileType) => + "Elle ${fileType} sera supprimée de votre appareil."; + + static String m54(fileType) => "Cette ${fileType} est à la fois sur ente et sur votre appareil."; - static String m54(fileType) => "Ce ${fileType} sera supprimé de ente."; + static String m55(fileType) => "Ce ${fileType} sera supprimé de ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} Go"; + static String m56(storageAmountInGB) => "${storageAmountInGB} Go"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} sur ${totalAmount} ${totalStorageUnit} utilisé"; - static String m57(id) => + static String m58(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 m58(endDate) => "Votre abonnement sera annulé le ${endDate}"; + static String m59(endDate) => "Votre abonnement sera annulé le ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} souvenirs préservés"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "Ils obtiennent aussi ${storageAmountInGB} Go"; - static String m61(email) => "Ceci est l\'ID de vérification de ${email}"; + static String m62(email) => "Ceci est l\'ID de vérification de ${email}"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '0 jour', one: '1 jour', other: '${count} jours')}"; - static String m63(endDate) => "Valable jusqu\'au ${endDate}"; + static String m64(endDate) => "Valable jusqu\'au ${endDate}"; - static String m64(email) => "Vérifier ${email}"; + static String m65(email) => "Vérifier ${email}"; - static String m65(email) => + static String m66(email) => "Nous avons envoyé un e-mail à ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: 'il y a ${count} an', other: 'il y a ${count} ans')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "Vous avez libéré ${storageSaved} avec succès !"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -217,14 +220,14 @@ class MessageLookup extends MessageLookupByLibrary { "Je comprends que si je perds mon mot de passe, je perdrai mes données puisque mes données sont chiffrées de bout en bout."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessions actives"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Ajouter un nouvel email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Ajouter un collaborateur"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("Ajouter depuis l\'appareil"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Ajouter la localisation"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Ajouter"), @@ -244,11 +247,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ajouter à un album masqué"), "addViewer": MessageLookupByLibrary.simpleMessage("Ajouter un observateur"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Ajoutez vos photos maintenant"), "addedAs": MessageLookupByLibrary.simpleMessage("Ajouté comme"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Ajout aux favoris..."), "advanced": MessageLookupByLibrary.simpleMessage("Avancé"), @@ -259,7 +263,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Après 1 semaine"), "after1Year": MessageLookupByLibrary.simpleMessage("Après 1 an"), "albumOwner": MessageLookupByLibrary.simpleMessage("Propriétaire"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Titre de l\'album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album mis à jour"), @@ -294,7 +298,7 @@ class MessageLookup extends MessageLookupByLibrary { "Android, iOS, Web, Ordinateur"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Authentification requise"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Appliquer"), "applyCodeTitle": @@ -384,10 +388,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Vous ne pouvez supprimer que les fichiers que vous possédez"), "cancel": MessageLookupByLibrary.simpleMessage("Annuler"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annuler l\'abonnement"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Les fichiers partagés ne peuvent pas être supprimés"), "centerPoint": MessageLookupByLibrary.simpleMessage("Point central"), @@ -410,7 +414,7 @@ class MessageLookup extends MessageLookupByLibrary { "Réclamer le stockage gratuit"), "claimMore": MessageLookupByLibrary.simpleMessage("Réclamez plus !"), "claimed": MessageLookupByLibrary.simpleMessage("Réclamée"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "clearCaches": MessageLookupByLibrary.simpleMessage("Nettoyer le cache"), "click": MessageLookupByLibrary.simpleMessage("• Click"), @@ -431,7 +435,7 @@ class MessageLookup extends MessageLookupByLibrary { "Créez un lien pour permettre aux gens d\'ajouter et de voir des photos dans votre album partagé sans avoir besoin d\'une application ente ou d\'un compte. Idéal pour collecter des photos d\'événement."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Lien collaboratif"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborateur"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -459,10 +463,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmer la clé de récupération"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirmer la clé de récupération"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacter l\'assistance"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contenus"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuer"), @@ -543,12 +547,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Supprimer de l\'appareil"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Supprimer de ente"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Supprimer la localisation"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Supprimer des photos"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Il manque une fonction clé dont j\'ai besoin"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -582,7 +586,7 @@ class MessageLookup extends MessageLookupByLibrary { "Les téléspectateurs peuvent toujours prendre des captures d\'écran ou enregistrer une copie de vos photos en utilisant des outils externes"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Veuillez remarquer"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Désactiver la double-authentification"), "disablingTwofactorAuthentication": @@ -603,9 +607,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Échec du téléchargement"), "downloading": MessageLookupByLibrary.simpleMessage("Téléchargement en cours..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -617,8 +621,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("éligible"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "Vérification de l\'adresse e-mail"), "emailYourLogs": @@ -717,12 +721,10 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Types de fichiers"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Types et noms de fichiers"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fichiers supprimés"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("Retourner"), "forYourMemories": MessageLookupByLibrary.simpleMessage("pour vos souvenirs"), @@ -730,19 +732,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Mot de passe oublié"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Stockage gratuit réclamé"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Stockage gratuit utilisable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Essai gratuit"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Libérer de l\'espace sur l\'appareil"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libérer de l\'espace"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Jusqu\'à 1000 souvenirs affichés dans la galerie"), "general": MessageLookupByLibrary.simpleMessage("Général"), @@ -809,7 +811,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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Les éléments montrent le nombre de jours restants avant la suppression définitive"), @@ -838,7 +840,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limite d\'appareil"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activé"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expiré"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiration du lien"), "linkHasExpired": @@ -887,6 +889,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Déconnexion"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Cela enverra des logs pour nous aider à déboguer votre problème. Veuillez noter que les noms de fichiers seront inclus pour aider à suivre les problèmes avec des fichiers spécifiques."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Appuyez longuement sur un élément pour le voir en plein écran"), @@ -904,7 +909,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Cartes"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Marchandise"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Ordinateur"), @@ -915,12 +920,12 @@ class MessageLookup extends MessageLookupByLibrary { "Modifiez votre requête, ou essayez de rechercher"), "moments": MessageLookupByLibrary.simpleMessage("Souvenirs"), "monthly": MessageLookupByLibrary.simpleMessage("Mensuel"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Déplacer vers l\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Déplacer vers un album masqué"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Déplacé dans la corbeille"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -984,15 +989,14 @@ class MessageLookup extends MessageLookupByLibrary { "Le mot de passe a été modifié"), "passwordLock": MessageLookupByLibrary.simpleMessage("Mot de passe verrou"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Nous ne stockons pas ce mot de passe, donc si vous l\'oubliez, nous ne pouvons pas déchiffrer vos données"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Détails de paiement"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Échec du paiement"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedTalkToProvider": m37, "pendingSync": MessageLookupByLibrary.simpleMessage("Synchronisation en attente"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -1015,7 +1019,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Sélectionner le point central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Épingler l\'album"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"), "pleaseContactSupportAndWeWillBeHappyToHelp": @@ -1024,12 +1028,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Merci de contacter l\'assistance si cette erreur persiste"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Veuillez accorder la permission"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Veuillez vous reconnecter"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Veuillez réessayer"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1065,7 +1069,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Évaluer l\'application"), "rateUs": MessageLookupByLibrary.simpleMessage("Évaluez-nous"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Récupérer"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Récupérer un compte"), @@ -1096,7 +1100,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Donnez ce code à vos amis"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ils s\'inscrivent à une offre payante"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Parrainages"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Les recommandations sont actuellement en pause"), @@ -1122,7 +1126,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Supprimer le participant"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien public"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1138,7 +1142,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Renommer le fichier"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renouveler l’abonnement"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), "resendEmail": @@ -1203,7 +1207,7 @@ class MessageLookup extends MessageLookupByLibrary { "Grouper les photos qui sont prises dans un certain angle d\'une photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Invitez des gens, et vous verrez ici toutes les photos qu\'ils partagent"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), @@ -1232,8 +1236,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront supprimés de tous les albums et déplacés dans la corbeille."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Envoyer"), "sendEmail": MessageLookupByLibrary.simpleMessage("Envoyer un e-mail"), "sendInvite": @@ -1259,16 +1263,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Partagez un album maintenant"), "shareLink": MessageLookupByLibrary.simpleMessage("Partager le lien"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partager uniquement avec les personnes que vous voulez"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Téléchargez ente pour que nous puissions facilement partager des photos et des vidéos de qualité originale\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partager avec des utilisateurs non-ente"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partagez votre premier album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1279,7 +1283,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nouvelles photos partagées"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recevoir des notifications quand quelqu\'un ajoute une photo à un album partagé dont vous faites partie"), - "sharedWith": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partagés avec moi"), "sharedWithYou": @@ -1289,11 +1293,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Montrer les souvenirs"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "J\'accepte les conditions d\'utilisation et la politique de confidentialité"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Elle sera supprimée de tous les albums."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Ignorer"), "social": MessageLookupByLibrary.simpleMessage("Réseaux Sociaux"), "someItemsAreInBothEnteAndYourDevice": @@ -1333,14 +1337,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Stockage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famille"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Vous"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limite de stockage atteinte"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Securité forte"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("S\'abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Il semble que votre abonnement ait expiré. Veuillez vous abonner pour activer le partage."), @@ -1357,7 +1361,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage( "Suggérer des fonctionnalités"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisation arrêtée ?"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1386,7 +1390,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ces éléments seront supprimés de votre appareil."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ils seront supprimés de tous les albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1402,7 +1406,7 @@ class MessageLookup extends MessageLookupByLibrary { "Cette adresse mail est déjà utilisé"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Cette image n\'a pas de données exif"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ceci est votre ID de vérification"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1418,7 +1422,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"), "trash": MessageLookupByLibrary.simpleMessage("Corbeille"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Réessayer"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activez la sauvegarde pour télécharger automatiquement les fichiers ajoutés à ce dossier de l\'appareil sur ente."), @@ -1476,7 +1480,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Utiliser la photo sélectionnée"), "usedSpace": MessageLookupByLibrary.simpleMessage("Mémoire utilisée"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "La vérification a échouée, veuillez réessayer"), @@ -1485,7 +1489,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Vérifier l\'email"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Vérifier le mot de passe"), @@ -1514,11 +1518,11 @@ 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": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Securité Faible"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bienvenue !"), "yearly": MessageLookupByLibrary.simpleMessage("Annuel"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Oui"), "yesCancel": MessageLookupByLibrary.simpleMessage("Oui, annuler"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1549,7 +1553,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": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Votre compte a été supprimé"), "yourMap": MessageLookupByLibrary.simpleMessage("Votre carte"), diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 9a6e4f8c6..c3cbb0b74 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -21,24 +21,30 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'it'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Aggiungi elemento', other: 'Aggiungi elementi')}"; - static String m2(emailOrName) => "Aggiunto da ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Aggiunto con successo su ${albumName}"; + static String m4(emailOrName) => "Aggiunto da ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Aggiunto con successo su ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'Nessun partecipante', one: '1 Partecipante', other: '${count} Partecipanti')}"; - static String m5(versionValue) => "Versione: ${versionValue}"; + static String m7(versionValue) => "Versione: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Annulla prima il tuo abbonamento esistente da ${paymentProvider}"; - static String m7(user) => + static String m9(user) => "${user} non sarà più in grado di aggiungere altre foto a questo album\n\nSarà ancora in grado di rimuovere le foto esistenti aggiunte da lui o lei"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Il tuo piano famiglia ha già richiesto ${storageAmountInGb} GB finora', @@ -46,158 +52,155 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Hai già richiesto ${storageAmountInGb} GB finora!', })}"; - static String m9(albumName) => "Link collaborativo creato per ${albumName}"; + static String m11(albumName) => "Link collaborativo creato per ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Contatta ${familyAdminEmail} per gestire il tuo abbonamento"; - static String m11(provider) => + static String m13(provider) => "Scrivi all\'indirizzo support@ente.io per gestire il tuo abbonamento ${provider}."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Elimina ${count} elemento', other: 'Elimina ${count} elementi')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Eliminazione di ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Questo rimuoverà il link pubblico per accedere a \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Per favore invia un\'email a ${supportEmail} dall\'indirizzo email con cui ti sei registrato"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "Hai ripulito ${Intl.plural(count, one: '${count} doppione', other: '${count} doppioni')}, salvando (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} file, ${formattedSize} l\'uno"; - static String m18(newEmail) => "Email cambiata in ${newEmail}"; + static String m20(newEmail) => "Email cambiata in ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} non ha un account su ente.\n\nInvia un invito per condividere foto."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} liberi"; - static String m24(endDate) => "La prova gratuita termina il ${endDate}"; + static String m26(endDate) => "La prova gratuita termina il ${endDate}"; - static String m25(count) => + static String m27(count) => "Puoi ancora accedere a ${Intl.plural(count, one: '', other: 'loro')} su ente finché hai un abbonamento attivo"; - static String m26(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(count, formattedSize) => "${Intl.plural(count, one: 'Può essere cancellata per liberare ${formattedSize}', other: 'Possono essere cancellati per liberare ${formattedSize}')}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementi')}"; - static String m30(expiryTime) => "Il link scadrà il ${expiryTime}"; + static String m32(expiryTime) => "Il link scadrà il ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} ricordo', other: '${formattedCount} ricordi')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Sposta elemento', other: 'Sposta elementi')}"; - static String m33(albumName) => "Spostato con successo su ${albumName}"; + static String m35(albumName) => "Spostato con successo su ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Sicurezza password: ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Si prega di parlare con il supporto di ${providerName} se ti è stato addebitato qualcosa"; - static String m36(reason) => - "Purtroppo il tuo pagamento non è riuscito a causa di ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Prova gratuita valida fino al ${endDate}.\nPuoi scegliere un piano a pagamento in seguito."; - static String m38(toEmail) => "Per favore invia un\'email a ${toEmail}"; + static String m39(toEmail) => "Per favore invia un\'email a ${toEmail}"; - static String m39(toEmail) => "Invia i log a \n${toEmail}"; + static String m40(toEmail) => "Invia i log a \n${toEmail}"; - static String m40(storeName) => "Valutaci su ${storeName}"; + static String m41(storeName) => "Valutaci su ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "3. Ottenete entrambi ${storageInGB} GB* gratis"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} verrà rimosso da questo album condiviso\n\nQualsiasi foto aggiunta dall\'utente verrà rimossa dall\'album"; - static String m43(endDate) => "Si rinnova il ${endDate}"; + static String m44(endDate) => "Si rinnova il ${endDate}"; - static String m45(count) => "${count} selezionati"; + static String m46(count) => "${count} selezionati"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} selezionato (${yourCount} tuoi)"; - static String m47(verificationID) => + static String m48(verificationID) => "Ecco il mio ID di verifica: ${verificationID} per ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hey, puoi confermare che questo è il tuo ID di verifica: ${verificationID} su ente.io"; - static String m49(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente referral code: ${referralCode} \n\nApplicalo in Impostazioni → Generale → Referral per ottenere ${referralStorageInGB} GB gratis dopo la registrazione di un piano a pagamento\n\nhttps://ente.io"; - static String m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Condividi con persone specifiche', one: 'Condividi con una persona', other: 'Condividi con ${numberOfPeople} persone')}"; - static String m51(emailIDs) => "Condiviso con ${emailIDs}"; - - static String m52(fileType) => - "Questo ${fileType} verrà eliminato dal tuo dispositivo."; + static String m52(emailIDs) => "Condiviso con ${emailIDs}"; static String m53(fileType) => + "Questo ${fileType} verrà eliminato dal tuo dispositivo."; + + static String m54(fileType) => "Questo ${fileType} è sia su ente che sul tuo dispositivo."; - static String m54(fileType) => "Questo ${fileType} verrà eliminato su ente."; + static String m55(fileType) => "Questo ${fileType} verrà eliminato su ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} di ${totalAmount} ${totalStorageUnit} utilizzati"; - static String m57(id) => + static String m58(id) => "Il tuo ${id} è già collegato ad un altro account ente.\nSe desideri utilizzare il tuo ${id} con questo account, contatta il nostro supporto\'\'"; - static String m58(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; + static String m59(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} ricordi conservati"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "Anche loro riceveranno ${storageAmountInGB} GB"; - static String m61(email) => "Questo è l\'ID di verifica di ${email}"; + static String m62(email) => "Questo è l\'ID di verifica di ${email}"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 giorno', other: '${count} giorni')}"; - static String m63(endDate) => "Valido fino al ${endDate}"; + static String m64(endDate) => "Valido fino al ${endDate}"; - static String m64(email) => "Verifica ${email}"; + static String m65(email) => "Verifica ${email}"; - static String m65(email) => + static String m66(email) => "Abbiamo inviato una mail a ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} anno fa', other: '${count} anni fa')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "Hai liberato con successo ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -212,14 +215,14 @@ class MessageLookup extends MessageLookupByLibrary { "Comprendo che se perdo la password potrei perdere l\'accesso ai miei dati poiché sono criptati end-to-end."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessioni attive"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Aggiungi una nuova email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Aggiungi collaboratore"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("Aggiungi dal dispositivo"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Aggiungi luogo"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Aggiungi"), "addMore": MessageLookupByLibrary.simpleMessage("Aggiungi altri"), @@ -236,9 +239,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi ad album nascosto"), "addViewer": MessageLookupByLibrary.simpleMessage("Aggiungi in sola lettura"), + "addViewers": m1, "addedAs": MessageLookupByLibrary.simpleMessage("Aggiunto come"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Aggiunto ai preferiti..."), "advanced": MessageLookupByLibrary.simpleMessage("Avanzate"), @@ -250,7 +254,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dopo una settimana"), "after1Year": MessageLookupByLibrary.simpleMessage("Dopo un anno"), "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietario"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Titolo album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album aggiornato"), @@ -287,7 +291,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Autenticazione necessaria"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Applica"), "applyCodeTitle": @@ -371,10 +375,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Puoi rimuovere solo i file di tua proprietà"), "cancel": MessageLookupByLibrary.simpleMessage("Annulla"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annulla abbonamento"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Impossibile eliminare i file condivisi"), "centerPoint": MessageLookupByLibrary.simpleMessage("Punto centrale"), @@ -397,7 +401,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Richiedi spazio gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("Richiedine di più!"), "claimed": MessageLookupByLibrary.simpleMessage("Riscattato"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "clearCaches": MessageLookupByLibrary.simpleMessage("Svuota cache"), "click": MessageLookupByLibrary.simpleMessage("• Clic"), "clickOnTheOverflowMenu": @@ -417,7 +421,7 @@ class MessageLookup extends MessageLookupByLibrary { "Crea un link per consentire alle persone di aggiungere e visualizzare foto nel tuo album condiviso senza bisogno di un\'applicazione o di un account ente. Ottimo per raccogliere foto di un evento."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link collaborativo"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Collaboratore"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -445,10 +449,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Conferma chiave di recupero"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Conferma la tua chiave di recupero"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contatta il supporto"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continua"), "continueOnFreeTrial": @@ -524,11 +528,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Elimina dal dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Elimina da ente"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Elimina posizione"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Elimina foto"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Manca una caratteristica chiave di cui ho bisogno"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -562,7 +566,7 @@ class MessageLookup extends MessageLookupByLibrary { "I visualizzatori possono scattare screenshot o salvare una copia delle foto utilizzando strumenti esterni"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Nota bene"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Disabilita autenticazione a due fattori"), "disablingTwofactorAuthentication": @@ -583,9 +587,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Scaricamento fallito"), "downloading": MessageLookupByLibrary.simpleMessage("Scaricamento in corso..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Modifica"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -596,8 +600,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("idoneo"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifica Email"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -690,11 +694,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi descrizione..."), "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("File salvato nella galleria"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("File eliminati"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("Capovolgi"), "forYourMemories": MessageLookupByLibrary.simpleMessage("per i tuoi ricordi"), @@ -702,18 +704,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Password dimenticata"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spazio gratuito richiesto"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spazio libero utilizzabile"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prova gratuita"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Fino a 1000 ricordi mostrati nella galleria"), "general": MessageLookupByLibrary.simpleMessage("Generali"), @@ -778,7 +780,7 @@ 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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"), @@ -807,7 +809,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limite dei dispositivi"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Attivato"), "linkExpired": MessageLookupByLibrary.simpleMessage("Scaduto"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Scadenza del link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Il link è scaduto"), @@ -855,6 +857,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Disconnetti"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Invia i log per aiutarci a risolvere il tuo problema. Si prega di notare che i nomi dei file saranno inclusi per aiutare a tenere traccia di problemi con file specifici."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Premi a lungo su un elemento per visualizzarlo a schermo intero"), @@ -873,7 +878,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Mappe"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), @@ -882,12 +887,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), "monthly": MessageLookupByLibrary.simpleMessage("Mensile"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Sposta nell\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Sposta in album nascosto"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Spostato nel cestino"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -951,15 +956,14 @@ class MessageLookup extends MessageLookupByLibrary { "Password modificata con successo"), "passwordLock": MessageLookupByLibrary.simpleMessage("Blocco con password"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Noi non memorizziamo la tua password, quindi se te la dimentichi, non possiamo decriptare i tuoi dati"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Dettagli di Pagamento"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Pagamento non riuscito"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedTalkToProvider": m37, "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronizzazione in sospeso"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -979,7 +983,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Selezionare il punto centrale"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fissa l\'album"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"), "pleaseContactSupportAndWeWillBeHappyToHelp": @@ -988,12 +992,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Riprova. Se il problema persiste, ti invitiamo a contattare l\'assistenza"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Concedi i permessi"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Effettua nuovamente l\'accesso"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage( @@ -1027,7 +1031,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Invia ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Valuta l\'app"), "rateUs": MessageLookupByLibrary.simpleMessage("Lascia una recensione"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recupera"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recupera account"), @@ -1059,7 +1063,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": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Invita un Amico"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "I referral code sono attualmente in pausa"), @@ -1083,7 +1087,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Elimina link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Rimuovi link pubblico"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1097,7 +1101,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rinomina file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Rinnova abbonamento"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Rinvia email"), @@ -1162,8 +1166,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Invia"), "sendEmail": MessageLookupByLibrary.simpleMessage("Invia email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Invita"), @@ -1187,16 +1191,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Condividi un album"), "shareLink": MessageLookupByLibrary.simpleMessage("Condividi link"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Condividi solo con le persone che vuoi"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Scarica ente in modo da poter facilmente condividere foto e video senza perdita di qualità\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Condividi con utenti che non hanno un account ente"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Condividi il tuo primo album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1207,7 +1211,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": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Condivisi con me"), "sharedWithYou": @@ -1217,11 +1221,11 @@ class MessageLookup extends MessageLookupByLibrary { "showMemories": MessageLookupByLibrary.simpleMessage("Mostra ricordi"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Accetto i termini di servizio e la politica sulla privacy"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Verrà eliminato da tutti gli album."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Salta"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1262,13 +1266,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famiglia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite d\'archiviazione superato"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Iscriviti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Sembra che il tuo abbonamento sia scaduto. Iscriviti per abilitare la condivisione."), @@ -1285,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggerisci una funzionalità"), "support": MessageLookupByLibrary.simpleMessage("Assistenza"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizzazione interrotta"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1314,7 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Questi file verranno eliminati dal tuo dispositivo."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Verranno eliminati da tutti gli album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1331,7 +1335,7 @@ class MessageLookup extends MessageLookupByLibrary { "Questo indirizzo email è già registrato"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Questa immagine non ha dati EXIF"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Questo è il tuo ID di verifica"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1347,7 +1351,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totale"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"), "trash": MessageLookupByLibrary.simpleMessage("Cestino"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Attiva il backup per caricare automaticamente i file aggiunti in questa cartella del dispositivo su ente."), @@ -1404,7 +1408,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usa la foto selezionata"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spazio utilizzato"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifica fallita, per favore prova di nuovo"), @@ -1412,7 +1416,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID di verifica"), "verify": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifica email"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Verifica password"), @@ -1439,11 +1443,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Non puoi modificare foto e album che non possiedi"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Debole"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bentornato/a!"), "yearly": MessageLookupByLibrary.simpleMessage("Annuale"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Si"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sì, cancella"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1473,7 +1477,7 @@ class MessageLookup extends MessageLookupByLibrary { "Non puoi condividere con te stesso"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Non hai nulla di archiviato."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Il tuo account è stato eliminato"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index 0b6e7fc6a..83d8495a0 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -20,11 +20,18 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ko'; + static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), + "addCollaborators": m0, "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "addViewers": m1, "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( "Change location of selected items?"), "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), @@ -36,10 +43,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index b4bd15c86..fe49550d6 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -21,27 +21,33 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'nl'; static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Bestand toevoegen', other: 'Bestanden toevoegen')}"; - static String m1(storageAmount, endDate) => + static String m3(storageAmount, endDate) => "Jouw ${storageAmount} add-on is geldig tot ${endDate}"; - static String m2(emailOrName) => "Toegevoegd door ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m3(albumName) => "Succesvol toegevoegd aan ${albumName}"; + static String m4(emailOrName) => "Toegevoegd door ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Succesvol toegevoegd aan ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'Geen deelnemers', one: '1 deelnemer', other: '${count} deelnemers')}"; - static String m5(versionValue) => "Versie: ${versionValue}"; + static String m7(versionValue) => "Versie: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Annuleer eerst uw bestaande abonnement bij ${paymentProvider}"; - static String m7(user) => + static String m9(user) => "${user} zal geen foto\'s meer kunnen toevoegen aan dit album\n\nDe gebruiker zal nog steeds bestaande foto\'s kunnen verwijderen die door hen zijn toegevoegd"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Jouw familie heeft ${storageAmountInGb} GB geclaimd tot nu toe', @@ -49,167 +55,164 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Je hebt ${storageAmountInGb} GB geclaimd tot nu toe!', })}"; - static String m9(albumName) => + static String m11(albumName) => "Gezamenlijke link aangemaakt voor ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw abonnement te beheren"; - static String m11(provider) => + static String m13(provider) => "Neem contact met ons op via support@ente.io om uw ${provider} abonnement te beheren."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Verwijder ${count} bestand', other: 'Verwijder ${count} bestanden')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Verwijderen van ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Dit verwijdert de openbare link voor toegang tot \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Stuur een e-mail naar ${supportEmail} vanaf het door jou geregistreerde e-mailadres"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "Je hebt ${Intl.plural(count, one: '${count} dubbel bestand', other: '${count} dubbele bestanden')} opgeruimd, totaal (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} bestanden, elk ${formattedSize}"; - static String m18(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; + static String m20(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} heeft geen ente account.\n\nStuur ze een uitnodiging om foto\'s te delen."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album is veilig geback-upt"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} vrij"; - static String m24(endDate) => "Gratis proefversie geldig tot ${endDate}"; + static String m26(endDate) => "Gratis proefversie geldig tot ${endDate}"; - static String m25(count) => + static String m27(count) => "U heeft nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op ente zolang u een actief abonnement heeft"; - static String m26(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + static String m28(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; - static String m27(count, formattedSize) => + static String m29(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 m28(currentlyProcessing, totalCount) => + static String m30(currentlyProcessing, totalCount) => "Verwerken van ${currentlyProcessing} / ${totalCount}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m30(expiryTime) => "Link vervalt op ${expiryTime}"; + static String m32(expiryTime) => "Link vervalt op ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: 'geen herinneringen', one: '${formattedCount} herinnering', other: '${formattedCount} herinneringen')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Bestand verplaatsen', other: 'Bestanden verplaatsen')}"; - static String m33(albumName) => "Succesvol verplaatst naar ${albumName}"; + static String m35(albumName) => "Succesvol verplaatst naar ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Wachtwoord sterkte: ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Praat met ${providerName} klantenservice als u in rekening bent gebracht"; - static String m36(reason) => - "Helaas is uw betaling mislukt vanwege ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Gratis proefperiode geldig tot ${endDate}.\nU kunt naderhand een betaald abonnement kiezen."; - static String m38(toEmail) => "Stuur ons een e-mail op ${toEmail}"; + static String m39(toEmail) => "Stuur ons een e-mail op ${toEmail}"; - static String m39(toEmail) => + static String m40(toEmail) => "Verstuur de logboeken alstublieft naar ${toEmail}"; - static String m40(storeName) => "Beoordeel ons op ${storeName}"; + static String m41(storeName) => "Beoordeel ons op ${storeName}"; - static String m41(storageInGB) => + static String m42(storageInGB) => "Jullie krijgen allebei ${storageInGB} GB* gratis"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto\'s worden ook uit het album verwijderd"; - static String m43(endDate) => "Wordt verlengd op ${endDate}"; + static String m44(endDate) => "Wordt verlengd op ${endDate}"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} resultaat gevonden', other: '${count} resultaten gevonden')}"; - static String m45(count) => "${count} geselecteerd"; + static String m46(count) => "${count} geselecteerd"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} geselecteerd (${yourCount} van jou)"; - static String m47(verificationID) => + static String m48(verificationID) => "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; - static String m48(verificationID) => + static String m49(verificationID) => "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(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 m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}"; - static String m51(emailIDs) => "Gedeeld met ${emailIDs}"; - - static String m52(fileType) => - "Deze ${fileType} zal worden verwijderd van jouw apparaat."; + static String m52(emailIDs) => "Gedeeld met ${emailIDs}"; static String m53(fileType) => - "Deze ${fileType} staat zowel in ente als op jouw apparaat."; + "Deze ${fileType} zal worden verwijderd van jouw apparaat."; static String m54(fileType) => + "Deze ${fileType} staat zowel in ente als op jouw apparaat."; + + static String m55(fileType) => "Deze ${fileType} zal worden verwijderd uit ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt"; - static String m57(id) => + static String m58(id) => "Uw ${id} is al aan een ander ente account gekoppeld.\nAls u uw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice"; - static String m58(endDate) => "Uw abonnement loopt af op ${endDate}"; + static String m59(endDate) => "Uw abonnement loopt af op ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} herinneringen bewaard"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "Zij krijgen ook ${storageAmountInGB} GB"; - static String m61(email) => "Dit is de verificatie-ID van ${email}"; + static String m62(email) => "Dit is de verificatie-ID van ${email}"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 dag', other: '${count} dagen')}"; - static String m63(endDate) => "Geldig tot ${endDate}"; + static String m64(endDate) => "Geldig tot ${endDate}"; - static String m64(email) => "Verifieer ${email}"; + static String m65(email) => "Verifieer ${email}"; - static String m65(email) => + static String m66(email) => "We hebben een e-mail gestuurd naar ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} jaar geleden', other: '${count} jaar geleden')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "Je hebt ${storageSaved} succesvol vrijgemaakt!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -224,14 +227,14 @@ class MessageLookup extends MessageLookupByLibrary { "Ik begrijp dat als ik mijn wachtwoord verlies, ik mijn gegevens kan verliezen omdat mijn gegevens end-to-end versleuteld zijn."), "activeSessions": MessageLookupByLibrary.simpleMessage("Actieve sessies"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Nieuw e-mailadres toevoegen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Samenwerker toevoegen"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("Toevoegen vanaf apparaat"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Locatie toevoegen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Toevoegen"), @@ -239,7 +242,7 @@ class MessageLookup extends MessageLookupByLibrary { "addNew": MessageLookupByLibrary.simpleMessage("Nieuwe toevoegen"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details van add-ons"), - "addOnValidTill": m1, + "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s toevoegen"), "addSelected": @@ -250,11 +253,12 @@ class MessageLookup extends MessageLookupByLibrary { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Toevoegen aan verborgen album"), "addViewer": MessageLookupByLibrary.simpleMessage("Voeg kijker toe"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Voeg nu je foto\'s toe"), "addedAs": MessageLookupByLibrary.simpleMessage("Toegevoegd als"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Toevoegen aan favorieten..."), "advanced": MessageLookupByLibrary.simpleMessage("Geavanceerd"), @@ -265,7 +269,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Na 1 week"), "after1Year": MessageLookupByLibrary.simpleMessage("Na 1 jaar"), "albumOwner": MessageLookupByLibrary.simpleMessage("Eigenaar"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Albumtitel"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album bijgewerkt"), @@ -301,7 +305,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Verificatie vereist"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Toepassen"), "applyCodeTitle": @@ -387,10 +391,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Kan alleen bestanden verwijderen die jouw eigendom zijn"), "cancel": MessageLookupByLibrary.simpleMessage("Annuleer"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement opzeggen"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Kan gedeelde bestanden niet verwijderen"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -414,7 +418,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim gratis opslag"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim meer!"), "claimed": MessageLookupByLibrary.simpleMessage("Geclaimd"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Ongecategoriseerd opschonen"), "clearCaches": MessageLookupByLibrary.simpleMessage("Cache legen"), @@ -437,7 +441,7 @@ class MessageLookup extends MessageLookupByLibrary { "Maak een link waarmee mensen foto\'s in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een ente app of account nodig hebben. Handig voor het verzamelen van foto\'s van evenementen."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Gezamenlijke link"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Samenwerker"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -465,10 +469,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacteer klantenservice"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contacten"), "contents": MessageLookupByLibrary.simpleMessage("Inhoud"), "continueLabel": MessageLookupByLibrary.simpleMessage("Doorgaan"), @@ -545,12 +549,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verwijder van apparaat"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Verwijder van ente"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Verwijder locatie"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Foto\'s verwijderen"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Ik mis een belangrijke functie"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -588,7 +592,7 @@ class MessageLookup extends MessageLookupByLibrary { "Kijkers kunnen nog steeds screenshots maken of een kopie van je foto\'s opslaan met behulp van externe tools"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Let op"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Tweestapsverificatie uitschakelen"), "disablingTwofactorAuthentication": @@ -609,9 +613,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Download mislukt"), "downloading": MessageLookupByLibrary.simpleMessage("Downloaden..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), "editLocation": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), @@ -624,8 +628,8 @@ class MessageLookup extends MessageLookupByLibrary { "Bewerkte locatie wordt alleen gezien binnen Ente"), "eligible": MessageLookupByLibrary.simpleMessage("gerechtigd"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-mailverificatie"), "emailYourLogs": @@ -729,12 +733,10 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Bestandstype"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Bestandstypen en namen"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("Omdraaien"), "forYourMemories": MessageLookupByLibrary.simpleMessage("voor uw herinneringen"), @@ -742,24 +744,24 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Wachtwoord vergeten"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis opslag geclaimd"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Gratis opslag bruikbaar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis proefversie"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Apparaatruimte vrijmaken"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Ruimte vrijmaken"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Tot 1000 herinneringen getoond in de galerij"), "general": MessageLookupByLibrary.simpleMessage("Algemeen"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Encryptiesleutels genereren..."), - "genericProgress": m28, + "genericProgress": m30, "goToSettings": MessageLookupByLibrary.simpleMessage("Ga naar instellingen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -820,7 +822,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": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"), @@ -847,7 +849,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Apparaat limiet"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ingeschakeld"), "linkExpired": MessageLookupByLibrary.simpleMessage("Verlopen"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Vervaldatum"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link is vervallen"), @@ -896,6 +898,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Uitloggen"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Dit zal logboeken verzenden om ons te helpen uw probleem op te lossen. Houd er rekening mee dat bestandsnamen zullen worden meegenomen om problemen met specifieke bestanden bij te houden."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Houd een bestand lang ingedrukt om te bekijken op volledig scherm"), "lostDevice": @@ -919,7 +924,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Kaarten"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobiel, Web, Desktop"), @@ -929,12 +934,12 @@ class MessageLookup extends MessageLookupByLibrary { "Pas je zoekopdracht aan of zoek naar"), "moments": MessageLookupByLibrary.simpleMessage("Momenten"), "monthly": MessageLookupByLibrary.simpleMessage("Maandelijks"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Verplaats naar album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Verplaatsen naar verborgen album"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Naar prullenbak verplaatst"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1004,15 +1009,14 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Wachtwoord succesvol aangepast"), "passwordLock": MessageLookupByLibrary.simpleMessage("Wachtwoord slot"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Wij slaan dit wachtwoord niet op, dus als je het vergeet, kunnen we je gegevens niet ontsleutelen"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Betaalgegevens"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Betaling mislukt"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"), "pendingSync": MessageLookupByLibrary.simpleMessage( @@ -1040,7 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"), "playOnTv": MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1052,12 +1056,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Neem contact op met klantenservice als het probleem aanhoudt"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Geef alstublieft toestemming"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Log opnieuw in"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Probeer het nog eens"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1092,7 +1096,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Meld probleem"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Beoordeel de app"), "rateUs": MessageLookupByLibrary.simpleMessage("Beoordeel ons"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Herstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Account herstellen"), @@ -1123,7 +1127,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Geef deze code aan je vrienden"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ze registreren voor een betaald plan"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referenties"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Verwijzingen zijn momenteel gepauzeerd"), @@ -1149,7 +1153,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Verwijder link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Verwijder publieke link"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1165,7 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bestandsnaam wijzigen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement verlengen"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Een fout melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fout melden"), "resendEmail": @@ -1227,7 +1231,7 @@ class MessageLookup extends MessageLookupByLibrary { "Foto\'s groeperen die in een bepaalde straal van een foto worden genomen"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Nodig mensen uit, en je ziet alle foto\'s die door hen worden gedeeld hier"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecteer een locatie"), @@ -1254,8 +1258,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Verzenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-mail versturen"), "sendInvite": @@ -1279,16 +1283,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Deel nu een album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link delen"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Deel alleen met de mensen die u wilt"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download ente zodat we gemakkelijk foto\'s en video\'s van originele kwaliteit kunnen delen\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Delen met niet-ente gebruikers"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1299,7 +1303,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": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Gedeeld met mij"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Gedeeld met jou"), @@ -1314,11 +1318,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Log uit op andere apparaten"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ik ga akkoord met de gebruiksvoorwaarden en privacybeleid"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Het wordt uit alle albums verwijderd."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Overslaan"), "social": MessageLookupByLibrary.simpleMessage("Sociale media"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1356,13 +1360,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Opslagruimte"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jij"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Opslaglimiet overschreden"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Sterk"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Abonneer"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat je abonnement is verlopen. Abonneer om delen mogelijk te maken."), @@ -1379,7 +1383,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Features voorstellen"), "support": MessageLookupByLibrary.simpleMessage("Ondersteuning"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisatie gestopt"), "syncing": MessageLookupByLibrary.simpleMessage("Synchroniseren..."), @@ -1407,7 +1411,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Deze bestanden zullen worden verwijderd van uw apparaat."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ze zullen uit alle albums worden verwijderd."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1423,7 +1427,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dit e-mailadres is al in gebruik"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Deze foto heeft geen exif gegevens"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Dit is uw verificatie-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1440,7 +1444,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totaal"), "totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"), "trash": MessageLookupByLibrary.simpleMessage("Prullenbak"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "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."), @@ -1496,7 +1500,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gebruik geselecteerde foto"), "usedSpace": MessageLookupByLibrary.simpleMessage("Gebruikte ruimte"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificatie mislukt, probeer het opnieuw"), @@ -1504,7 +1508,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verificatie ID"), "verify": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Bevestig wachtwoord"), @@ -1533,11 +1537,11 @@ 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": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Zwak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welkom terug!"), "yearly": MessageLookupByLibrary.simpleMessage("Jaarlijks"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, opzeggen"), "yesConvertToViewer": @@ -1567,7 +1571,7 @@ class MessageLookup extends MessageLookupByLibrary { "Je kunt niet met jezelf delen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "U heeft geen gearchiveerde bestanden."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "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 e0891fc1c..05477cfeb 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -20,13 +20,20 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'no'; + static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Velkommen tilbake!"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), + "addCollaborators": m0, "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "addViewers": m1, "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Hva er hovedårsaken til at du sletter kontoen din?"), "cancel": MessageLookupByLibrary.simpleMessage("Avbryt"), @@ -54,14 +61,15 @@ class MessageLookup extends MessageLookupByLibrary { "Skriv inn e-postadressen din"), "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index 8faaae6f9..af6ba4353 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -20,7 +20,13 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pl'; - static String m34(passwordStrengthValue) => + static String m0(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m1(count) => + "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + + static String m36(passwordStrengthValue) => "Siła hasła: ${passwordStrengthValue}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -28,9 +34,10 @@ class MessageLookup extends MessageLookupByLibrary { "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Witaj ponownie!"), "activeSessions": MessageLookupByLibrary.simpleMessage("Aktywne sesje"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), + "addCollaborators": m0, "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Add to hidden album"), + "addViewers": m1, "askDeleteReason": MessageLookupByLibrary.simpleMessage( "Jaka jest przyczyna usunięcia konta?"), "cancel": MessageLookupByLibrary.simpleMessage("Anuluj"), @@ -102,8 +109,6 @@ class MessageLookup extends MessageLookupByLibrary { "Wprowadź swój klucz odzyskiwania"), "feedback": MessageLookupByLibrary.simpleMessage("Informacja zwrotna"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Nie pamiętam hasła"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( @@ -122,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), "logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Long press an email to verify end to end encryption."), "moderateStrength": MessageLookupByLibrary.simpleMessage("Umiarkowana"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( @@ -137,7 +145,7 @@ class MessageLookup extends MessageLookupByLibrary { "password": MessageLookupByLibrary.simpleMessage("Hasło"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Hasło zostało pomyślnie zmienione"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Nie przechowujemy tego hasła, więc jeśli go zapomnisz, nie będziemy w stanie odszyfrować Twoich danych"), "pleaseTryAgain": diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 04e6aaca3..d4d571d12 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -21,27 +21,33 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt'; static String m0(count) => + "${Intl.plural(count, zero: 'Adicionar colaborador', one: 'Adicionar coloborador', other: 'Adicionar colaboradores')}"; + + static String m2(count) => "${Intl.plural(count, one: 'Adicionar item', other: 'Adicionar itens')}"; - static String m1(storageAmount, endDate) => + static String m3(storageAmount, endDate) => "Seu complemento ${storageAmount} é válido até o dia ${endDate}"; - static String m2(emailOrName) => "Adicionado por ${emailOrName}"; + static String m1(count) => + "${Intl.plural(count, zero: 'Adicionar visualizador', one: 'Adicionar visualizador', other: 'Adicionar Visualizadores')}"; - static String m3(albumName) => "Adicionado com sucesso a ${albumName}"; + static String m4(emailOrName) => "Adicionado por ${emailOrName}"; - static String m4(count) => + static String m5(albumName) => "Adicionado com sucesso a ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: 'Nenhum Participante', one: '1 Participante', other: '${count} Participantes')}"; - static String m5(versionValue) => "Versão: ${versionValue}"; + static String m7(versionValue) => "Versão: ${versionValue}"; - static String m6(paymentProvider) => + static String m8(paymentProvider) => "Por favor, cancele sua assinatura existente do ${paymentProvider} primeiro"; - static String m7(user) => + static String m9(user) => "${user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda poderão remover as fotos existentes adicionadas por eles"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Sua família reeinvindicou ${storageAmountInGb} GB até agora', @@ -49,163 +55,160 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Você reeinvindicou ${storageAmountInGb} GB até agora', })}"; - static String m9(albumName) => "Link colaborativo criado para ${albumName}"; + static String m11(albumName) => "Link colaborativo criado para ${albumName}"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para gerenciar sua assinatura"; - static String m11(provider) => + static String m13(provider) => "Entre em contato conosco pelo e-mail support@ente.io para gerenciar sua assinatura ${provider}."; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "Excluindo ${currentlyDeleting} / ${totalCount}"; - static String m14(albumName) => + static String m16(albumName) => "Isso removerá o link público para acessar \"${albumName}\"."; - static String m15(supportEmail) => + static String m17(supportEmail) => "Por favor, envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registrado"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, salvando (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} Arquivos, ${formattedSize} cada"; - static String m18(newEmail) => "E-mail alterado para ${newEmail}"; + static String m20(newEmail) => "E-mail alterado para ${newEmail}"; - static String m19(email) => + static String m21(email) => "${email} Não possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste álbum teve um backup seguro"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} grátis"; - static String m24(endDate) => "Teste gratuito acaba em ${endDate}"; + static String m26(endDate) => "Teste gratuito acaba em ${endDate}"; - static String m25(count) => + static String m27(count) => "Você ainda pode acessar ${Intl.plural(count, one: 'ele', other: 'eles')} no ente contanto que você tenha uma assinatura ativa"; - static String m26(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(count, formattedSize) => "${Intl.plural(count, one: 'Pode ser excluído do dispositivo para liberar ${formattedSize}', other: 'Eles podem ser excluídos do dispositivo para liberar ${formattedSize}')}"; - static String m28(currentlyProcessing, totalCount) => + static String m30(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m30(expiryTime) => "O link irá expirar em ${expiryTime}"; + static String m32(expiryTime) => "O link irá expirar em ${expiryTime}"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: 'no memories', one: '${formattedCount} memory', other: '${formattedCount} memories')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; - static String m33(albumName) => "Movido com sucesso para ${albumName}"; + static String m35(albumName) => "Movido com sucesso para ${albumName}"; - static String m34(passwordStrengthValue) => + static String m36(passwordStrengthValue) => "Segurança da senha: ${passwordStrengthValue}"; - static String m35(providerName) => + static String m37(providerName) => "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m36(reason) => - "Infelizmente o seu pagamento falhou devido a ${reason}"; - - static String m37(endDate) => + static String m38(endDate) => "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m38(toEmail) => + static String m39(toEmail) => "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m39(toEmail) => "Por favor, envie os logs para \n${toEmail}"; + static String m40(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m40(storeName) => "Avalie-nos em ${storeName}"; + static String m41(storeName) => "Avalie-nos em ${storeName}"; - static String m41(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; + static String m42(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; - static String m43(endDate) => "Renovação de assinatura em ${endDate}"; + static String m44(endDate) => "Renovação de assinatura em ${endDate}"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultado encontrado')}"; - static String m45(count) => "${count} Selecionados"; + static String m46(count) => "${count} Selecionados"; - static String m46(count, yourCount) => + static String m47(count, yourCount) => "${count} Selecionado (${yourCount} seus)"; - static String m47(verificationID) => + static String m48(verificationID) => "Aqui está meu ID de verificação para o Ente.io: ${verificationID}"; - static String m48(verificationID) => + static String m49(verificationID) => "Ei, você pode confirmar que este é seu ID de verificação do Ente.io? ${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "Código de referência do ente: ${referralCode} \n\nAplique em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição em um plano pago\n\nhttps://ente.io"; - static String m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m51(emailIDs) => "Compartilhado com ${emailIDs}"; - - static String m52(fileType) => - "Este ${fileType} será excluído do seu dispositivo."; + static String m52(emailIDs) => "Compartilhado com ${emailIDs}"; static String m53(fileType) => + "Este ${fileType} será excluído do seu dispositivo."; + + static String m54(fileType) => "Este ${fileType} está em ente e no seu dispositivo."; - static String m54(fileType) => "Este ${fileType} será excluído do ente."; + static String m55(fileType) => "Este ${fileType} será excluído do ente."; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m57(id) => + static String m58(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 m58(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m59(endDate) => "Sua assinatura será cancelada em ${endDate}"; - static String m59(completed, total) => + static String m60(completed, total) => "${completed}/${total} memórias preservadas"; - static String m60(storageAmountInGB) => + static String m61(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m61(email) => "Este é o ID de verificação de ${email}"; + static String m62(email) => "Este é o ID de verificação de ${email}"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 dia', other: '${count} dias')}"; - static String m63(endDate) => "Válido até ${endDate}"; + static String m64(endDate) => "Válido até ${endDate}"; - static String m64(email) => "Verificar ${email}"; + static String m65(email) => "Verificar ${email}"; - static String m65(email) => "Enviamos um e-mail à ${email}"; + static String m66(email) => "Enviamos um e-mail à ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} anos atrás', other: '${count} anos atrás')}"; - static String m67(storageSaved) => + static String m68(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -224,16 +227,17 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adicionar um novo email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Adicionar colaborador"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage( "Adicionar a partir do dispositivo"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("Adicionar local"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Adicionar"), "addMore": MessageLookupByLibrary.simpleMessage("Adicione mais"), "addNew": MessageLookupByLibrary.simpleMessage("Adicionar novo"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Detalhes dos complementos"), - "addOnValidTill": m1, + "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adicionar fotos"), "addSelected": @@ -245,11 +249,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adicionar a álbum oculto"), "addViewer": MessageLookupByLibrary.simpleMessage("Adicionar visualizador"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Adicione suas fotos agora"), "addedAs": MessageLookupByLibrary.simpleMessage("Adicionado como"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage( "Adicionando aos favoritos..."), "advanced": MessageLookupByLibrary.simpleMessage("Avançado"), @@ -260,7 +265,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Após 1 semana"), "after1Year": MessageLookupByLibrary.simpleMessage("Após 1 ano"), "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietário"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("Título do álbum"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Álbum atualizado"), @@ -297,14 +302,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Autenticação necessária"), - "appVersion": m5, + "appVersion": m7, "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"), + "archive": MessageLookupByLibrary.simpleMessage("Arquivado"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), "areYouSureThatYouWantToLeaveTheFamily": @@ -384,10 +389,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Só é possível remover arquivos de sua propriedade"), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar assinatura"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Não é possível excluir arquivos compartilhados"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -411,7 +416,7 @@ class MessageLookup extends MessageLookupByLibrary { "Reivindicar armazenamento gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Limpar Sem Categoria"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -436,7 +441,7 @@ class MessageLookup extends MessageLookupByLibrary { "Crie um link para permitir pessoas adicionar e ver fotos no seu álbum compartilhado sem a necessidade do aplicativo ou uma conta Ente. Ótimo para colecionar fotos de eventos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link Colaborativo"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -463,10 +468,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirme a chave de recuperação"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirme sua chave de recuperação"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contate o suporte"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("Contatos"), "contents": MessageLookupByLibrary.simpleMessage("Conteúdos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), @@ -515,7 +520,7 @@ class MessageLookup extends MessageLookupByLibrary { "decryptingVideo": MessageLookupByLibrary.simpleMessage("Descriptografando vídeo..."), "deduplicateFiles": - MessageLookupByLibrary.simpleMessage("Arquivos Deduplicados"), + MessageLookupByLibrary.simpleMessage("Arquivos duplicados"), "delete": MessageLookupByLibrary.simpleMessage("Apagar"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( @@ -542,10 +547,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Excluir do ente"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Excluir Local"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Está faltando um recurso-chave que eu preciso"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -582,7 +587,7 @@ class MessageLookup extends MessageLookupByLibrary { "Os espectadores ainda podem tirar screenshots ou salvar uma cópia de suas fotos usando ferramentas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Observe"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Desativar autenticação de dois fatores"), "disablingTwofactorAuthentication": @@ -605,9 +610,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Falha ao baixar"), "downloading": MessageLookupByLibrary.simpleMessage("Baixando..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Editar local"), "editLocationTagTitle": @@ -618,8 +623,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edições para local só serão vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegível"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação de e-mail"), "emailYourLogs": @@ -718,8 +723,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( @@ -731,24 +736,24 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Armazenamento gratuito reivindicado"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Armazenamento livre utilizável"), "freeTrial": MessageLookupByLibrary.simpleMessage("Teste gratuito"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Liberar espaço no dispositivo"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espaço"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Até 1000 memórias mostradas na galeria"), "general": MessageLookupByLibrary.simpleMessage("Geral"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Gerando chaves de criptografia..."), - "genericProgress": m28, + "genericProgress": m30, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir para Configurações"), "googlePlayId": @@ -763,7 +768,8 @@ class MessageLookup extends MessageLookupByLibrary { "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "Como você ouviu sobre o Ente? (opcional)"), - "hidden": MessageLookupByLibrary.simpleMessage("Escondido"), + "help": MessageLookupByLibrary.simpleMessage("Ajuda"), + "hidden": MessageLookupByLibrary.simpleMessage("Oculto"), "hide": MessageLookupByLibrary.simpleMessage("Ocultar"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), "hostedAtOsmFrance": @@ -810,7 +816,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Parece que algo deu errado. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contato com nossa equipe de suporte."), - "itemCount": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens mostram o número de dias restantes antes da exclusão permanente"), @@ -838,7 +844,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limite do dispositivo"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("O link expirou"), @@ -888,6 +894,9 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("Encerrar sessão"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Isso enviará através dos logs para nos ajudar a depurar o seu problema. Por favor, note que nomes de arquivos serão incluídos para ajudar a rastrear problemas com arquivos específicos."), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage( + "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Pressione e segure em um item para exibir em tela cheia"), @@ -911,7 +920,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), @@ -921,11 +930,11 @@ class MessageLookup extends MessageLookupByLibrary { "Modifique sua consulta ou tente procurar por"), "moments": MessageLookupByLibrary.simpleMessage("Momentos"), "monthly": MessageLookupByLibrary.simpleMessage("Mensal"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover para álbum oculto"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -998,15 +1007,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), "passwordLock": MessageLookupByLibrary.simpleMessage("Bloqueio de senha"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "Nós não salvamos essa senha, se você esquecer nós não poderemos descriptografar seus dados"), "paymentDetails": MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Falha no pagamento"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), + "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1032,7 +1042,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixar álbum"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1044,12 +1054,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, faça login novamente"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1086,7 +1096,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1118,7 +1128,7 @@ class MessageLookup extends MessageLookupByLibrary { "Envie esse código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles se inscreveram para um plano pago"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Indicações"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referências estão atualmente pausadas"), @@ -1142,7 +1152,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Remover link público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1156,7 +1166,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar um problema"), "reportBug": @@ -1221,7 +1231,7 @@ class MessageLookup extends MessageLookupByLibrary { "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"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecionar um local"), @@ -1249,8 +1259,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo."), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1274,16 +1284,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Baixe o Ente para podermos compartilhar facilmente fotos e vídeos de alta qualidade\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários não-Ente"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Compartilhar seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1296,7 +1306,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adicionar uma foto em um álbum compartilhado que você faz parte"), - "sharedWith": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), "sharedWithYou": @@ -1312,11 +1322,11 @@ class MessageLookup extends MessageLookupByLibrary { "Terminar sessão em outros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), "someItemsAreInBothEnteAndYourDevice": @@ -1357,13 +1367,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Assinar"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Parece que sua assinatura expirou. Por favor inscreva-se para ativar o compartilhamento."), @@ -1378,9 +1388,9 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("Desocultado com sucesso"), "suggestFeatures": - MessageLookupByLibrary.simpleMessage("Sugerir funcionalidades"), + MessageLookupByLibrary.simpleMessage("Sugerir recurso"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1407,7 +1417,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1423,7 +1433,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Este e-mail já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagem não tem dados exif"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1439,7 +1449,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o ente."), @@ -1492,7 +1502,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço em uso"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação, por favor, tente novamente"), @@ -1500,7 +1510,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -1533,12 +1543,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo de volta!"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1569,7 +1579,7 @@ class MessageLookup extends MessageLookupByLibrary { "Você não pode compartilhar consigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 88c0f53c4..c611a7352 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -21,174 +21,178 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh'; static String m0(count) => + "${Intl.plural(count, zero: '添加协作者', one: '添加协作者', other: '添加协作者')}"; + + static String m2(count) => "${Intl.plural(count, one: '添加一个项目', other: '添加一些项目')}"; - static String m1(storageAmount, endDate) => + static String m3(storageAmount, endDate) => "您的 ${storageAmount} 插件有效期至 ${endDate}"; - static String m2(emailOrName) => "由 ${emailOrName} 添加"; + static String m1(count) => + "${Intl.plural(count, zero: '添加查看者', one: '添加查看者', other: '添加查看者')}"; - static String m3(albumName) => "成功添加到 ${albumName}"; + static String m4(emailOrName) => "由 ${emailOrName} 添加"; - static String m4(count) => + static String m5(albumName) => "成功添加到 ${albumName}"; + + static String m6(count) => "${Intl.plural(count, zero: '无参与者', one: '1个参与者', other: '${count} 个参与者')}"; - static String m5(versionValue) => "版本: ${versionValue}"; + static String m7(versionValue) => "版本: ${versionValue}"; - static String m6(paymentProvider) => "请先取消您现有的订阅 ${paymentProvider}"; + static String m8(paymentProvider) => "请先取消您现有的订阅 ${paymentProvider}"; - static String m7(user) => "${user} 将无法添加更多照片到此相册\n\n他们仍然能够删除他们添加的现有照片"; + static String m9(user) => "${user} 将无法添加更多照片到此相册\n\n他们仍然能够删除他们添加的现有照片"; - static String m8(isFamilyMember, storageAmountInGb) => + static String m10(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': '到目前为止,您的家庭已经领取了 ${storageAmountInGb} GB', 'false': '到目前为止,您已经领取了 ${storageAmountInGb} GB', 'other': '到目前为止,您已经领取了${storageAmountInGb} GB', })}"; - static String m9(albumName) => "为 ${albumName} 创建了协作链接"; + static String m11(albumName) => "为 ${albumName} 创建了协作链接"; - static String m10(familyAdminEmail) => + static String m12(familyAdminEmail) => "请联系 ${familyAdminEmail} 来管理您的订阅"; - static String m11(provider) => + static String m13(provider) => "请通过support@ente.io 用英语联系我们来管理您的 ${provider} 订阅。"; - static String m12(count) => + static String m14(count) => "${Intl.plural(count, one: '删除 ${count} 个项目', other: '删除 ${count} 个项目')}"; - static String m13(currentlyDeleting, totalCount) => + static String m15(currentlyDeleting, totalCount) => "正在删除 ${currentlyDeleting} /共 ${totalCount}"; - static String m14(albumName) => "这将删除用于访问\"${albumName}\"的公共链接。"; + static String m16(albumName) => "这将删除用于访问\"${albumName}\"的公共链接。"; - static String m15(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}"; + static String m17(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}"; - static String m16(count, storageSaved) => + static String m18(count, storageSaved) => "您已经清理了 ${Intl.plural(count, other: '${count} 个重复文件')}, 释放了 (${storageSaved}!)"; - static String m17(count, formattedSize) => + static String m19(count, formattedSize) => "${count} 个文件,每个文件 ${formattedSize}"; - static String m18(newEmail) => "电子邮件已更改为 ${newEmail}"; + static String m20(newEmail) => "电子邮件已更改为 ${newEmail}"; - static String m19(email) => "${email} 没有 ente 账户。\n\n向他们发送分享照片的邀请。"; + static String m21(email) => "${email} 没有 ente 账户。\n\n向他们发送分享照片的邀请。"; - static String m20(count, formattedNumber) => + static String m22(count, formattedNumber) => "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; - static String m21(count, formattedNumber) => + static String m23(count, formattedNumber) => "此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; - static String m22(storageAmountInGB) => + static String m24(storageAmountInGB) => "每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB"; - static String m23(freeAmount, storageUnit) => + static String m25(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} 空闲"; - static String m24(endDate) => "免费试用有效期至 ${endDate}"; + static String m26(endDate) => "免费试用有效期至 ${endDate}"; - static String m25(count) => + static String m27(count) => "只要您有有效的订阅,您仍然可以在 ente 上访问 ${Intl.plural(count, one: 'it', other: 'them')}"; - static String m26(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; + static String m28(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; - static String m27(count, formattedSize) => + static String m29(count, formattedSize) => "${Intl.plural(count, one: '它可以从设备中删除以释放 ${formattedSize}', other: '它们可以从设备中删除以释放 ${formattedSize}')}"; - static String m28(currentlyProcessing, totalCount) => + static String m30(currentlyProcessing, totalCount) => "正在处理 ${currentlyProcessing} / ${totalCount}"; - static String m29(count) => + static String m31(count) => "${Intl.plural(count, one: '${count} 个项目', other: '${count} 个项目')}"; - static String m30(expiryTime) => "链接将在 ${expiryTime} 过期"; + static String m32(expiryTime) => "链接将在 ${expiryTime} 过期"; - static String m31(count, formattedCount) => + static String m33(count, formattedCount) => "${Intl.plural(count, zero: '没有回忆', one: '${formattedCount} 个回忆', other: '${formattedCount} 个回忆')}"; - static String m32(count) => + static String m34(count) => "${Intl.plural(count, one: '移动一个项目', other: '移动一些项目')}"; - static String m33(albumName) => "成功移动到 ${albumName}"; + static String m35(albumName) => "成功移动到 ${albumName}"; - static String m34(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; + static String m36(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; - static String m35(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; + static String m37(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; - static String m36(reason) => "很抱歉,您的支付因 ${reason} 而失败"; + static String m38(endDate) => "免费试用有效期至 ${endDate}。\n之后您可以选择付费计划。"; - static String m37(endDate) => "免费试用有效期至 ${endDate}。\n之后您可以选择付费计划。"; + static String m39(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; - static String m38(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; + static String m40(toEmail) => "请将日志发送至 \n${toEmail}"; - static String m39(toEmail) => "请将日志发送至 \n${toEmail}"; + static String m41(storeName) => "在 ${storeName} 上给我们评分"; - static String m40(storeName) => "在 ${storeName} 上给我们评分"; + static String m42(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - static String m41(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - - static String m42(userEmail) => + static String m43(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; - static String m43(endDate) => "在 ${endDate} 前续费"; + static String m44(endDate) => "在 ${endDate} 前续费"; - static String m44(count) => + static String m45(count) => "${Intl.plural(count, other: '已找到 ${count} 个结果')}"; - static String m45(count) => "已选择 ${count} 个"; + static String m46(count) => "已选择 ${count} 个"; - static String m46(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; + static String m47(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; - static String m47(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; + static String m48(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; - static String m48(verificationID) => + static String m49(verificationID) => "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; - static String m49(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente推荐码: ${referralCode} \n\n注册付费计划后在设置 → 常规 → 推荐中应用它以免费获得 ${referralStorageInGB} GB空间\n\nhttps://ente.io"; - static String m50(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; - static String m51(emailIDs) => "与 ${emailIDs} 共享"; + static String m52(emailIDs) => "与 ${emailIDs} 共享"; - static String m52(fileType) => "此 ${fileType} 将从您的设备中删除。"; + static String m53(fileType) => "此 ${fileType} 将从您的设备中删除。"; - static String m53(fileType) => "此 ${fileType} 同时在ente和您的设备中。"; + static String m54(fileType) => "此 ${fileType} 同时在ente和您的设备中。"; - static String m54(fileType) => "此 ${fileType} 将从ente中删除。"; + static String m55(fileType) => "此 ${fileType} 将从ente中删除。"; - static String m55(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m56( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "已使用 ${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit}"; - static String m57(id) => + static String m58(id) => "您的 ${id} 已经链接到另一个ente账户。\n如果您想要通过此账户使用您的 ${id} ,请联系我们的客服\'\'"; - static String m58(endDate) => "您的订阅将于 ${endDate} 取消"; + static String m59(endDate) => "您的订阅将于 ${endDate} 取消"; - static String m59(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; + static String m60(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; - static String m60(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; + static String m61(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; - static String m61(email) => "这是 ${email} 的验证ID"; + static String m62(email) => "这是 ${email} 的验证ID"; - static String m62(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1天', other: '${count} 天')}"; - static String m63(endDate) => "有效期至 ${endDate}"; + static String m64(endDate) => "有效期至 ${endDate}"; - static String m64(email) => "验证 ${email}"; + static String m65(email) => "验证 ${email}"; - static String m65(email) => "我们已经发送邮件到 ${email}"; + static String m66(email) => "我们已经发送邮件到 ${email}"; - static String m66(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m67(storageSaved) => "您已成功释放了 ${storageSaved}!"; + static String m68(storageSaved) => "您已成功释放了 ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -200,17 +204,17 @@ class MessageLookup extends MessageLookupByLibrary { "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "我明白,如果我丢失密码,我可能会丢失我的数据,因为我的数据是 端到端加密的。"), "activeSessions": MessageLookupByLibrary.simpleMessage("已登录的设备"), - "addAName": MessageLookupByLibrary.simpleMessage("Add a name"), "addANewEmail": MessageLookupByLibrary.simpleMessage("添加新的电子邮件"), "addCollaborator": MessageLookupByLibrary.simpleMessage("添加协作者"), + "addCollaborators": m0, "addFromDevice": MessageLookupByLibrary.simpleMessage("从设备添加"), - "addItem": m0, + "addItem": m2, "addLocation": MessageLookupByLibrary.simpleMessage("添加地点"), "addLocationButton": MessageLookupByLibrary.simpleMessage("添加"), "addMore": MessageLookupByLibrary.simpleMessage("添加更多"), "addNew": MessageLookupByLibrary.simpleMessage("新建"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("附加组件详情"), - "addOnValidTill": m1, + "addOnValidTill": m3, "addOns": MessageLookupByLibrary.simpleMessage("附加组件"), "addPhotos": MessageLookupByLibrary.simpleMessage("添加照片"), "addSelected": MessageLookupByLibrary.simpleMessage("添加所选项"), @@ -218,10 +222,11 @@ class MessageLookup extends MessageLookupByLibrary { "addToEnte": MessageLookupByLibrary.simpleMessage("添加到 ente"), "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("添加到隐藏相册"), "addViewer": MessageLookupByLibrary.simpleMessage("添加查看者"), + "addViewers": m1, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("立即添加您的照片"), "addedAs": MessageLookupByLibrary.simpleMessage("已添加为"), - "addedBy": m2, - "addedSuccessfullyTo": m3, + "addedBy": m4, + "addedSuccessfullyTo": m5, "addingToFavorites": MessageLookupByLibrary.simpleMessage("正在添加到收藏..."), "advanced": MessageLookupByLibrary.simpleMessage("高级设置"), "advancedSettings": MessageLookupByLibrary.simpleMessage("高级设置"), @@ -231,7 +236,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("1 周后"), "after1Year": MessageLookupByLibrary.simpleMessage("1 年后"), "albumOwner": MessageLookupByLibrary.simpleMessage("所有者"), - "albumParticipantsCount": m4, + "albumParticipantsCount": m6, "albumTitle": MessageLookupByLibrary.simpleMessage("相册标题"), "albumUpdated": MessageLookupByLibrary.simpleMessage("相册已更新"), "albums": MessageLookupByLibrary.simpleMessage("相册"), @@ -260,7 +265,7 @@ class MessageLookup extends MessageLookupByLibrary { "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage("安卓, iOS, 网页端, 桌面端"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("需要身份验证"), - "appVersion": m5, + "appVersion": m7, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("应用"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("应用代码"), @@ -332,9 +337,9 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage("只能删除您拥有的文件"), "cancel": MessageLookupByLibrary.simpleMessage("取消"), - "cancelOtherSubscription": m6, + "cancelOtherSubscription": m8, "cancelSubscription": MessageLookupByLibrary.simpleMessage("取消订阅"), - "cannotAddMorePhotosAfterBecomingViewer": m7, + "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("无法删除共享文件"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -353,7 +358,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimFreeStorage": MessageLookupByLibrary.simpleMessage("领取免费存储"), "claimMore": MessageLookupByLibrary.simpleMessage("领取更多!"), "claimed": MessageLookupByLibrary.simpleMessage("已领取"), - "claimedStorageSoFar": m8, + "claimedStorageSoFar": m10, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("清除未分类的"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage("从“未分类”中删除其他相册中存在的所有文件"), @@ -372,7 +377,7 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "创建一个链接以允许其他人在您的共享相册中添加和查看照片,而无需应用程序或ente账户。 非常适合收集活动照片。"), "collaborativeLink": MessageLookupByLibrary.simpleMessage("协作链接"), - "collaborativeLinkCreatedFor": m9, + "collaborativeLinkCreatedFor": m11, "collaborator": MessageLookupByLibrary.simpleMessage("协作者"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage("协作者可以将照片和视频添加到共享相册中。"), @@ -393,9 +398,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage("确认恢复密钥"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("确认您的恢复密钥"), - "contactFamilyAdmin": m10, + "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("联系支持"), - "contactToManageSubscription": m11, + "contactToManageSubscription": m13, "contacts": MessageLookupByLibrary.simpleMessage("联系人"), "contents": MessageLookupByLibrary.simpleMessage("内容"), "continueLabel": MessageLookupByLibrary.simpleMessage("继续"), @@ -453,10 +458,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromBoth": MessageLookupByLibrary.simpleMessage("同时从两者中删除"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("从设备中删除"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("从ente 中删除"), - "deleteItemCount": m12, + "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("删除位置"), "deletePhotos": MessageLookupByLibrary.simpleMessage("删除照片"), - "deleteProgress": m13, + "deleteProgress": m15, "deleteReason1": MessageLookupByLibrary.simpleMessage("找不到我想要的功能"), "deleteReason2": MessageLookupByLibrary.simpleMessage("应用或某个功能没有按我的预期运行"), @@ -468,6 +473,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage("要删除共享相册吗?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "将为所有人删除相册\n\n您将无法访问此相册中他人拥有的共享照片"), + "descriptions": MessageLookupByLibrary.simpleMessage("描述"), "deselectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("经久耐用"), "details": MessageLookupByLibrary.simpleMessage("详情"), @@ -485,7 +491,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("查看者仍然可以使用外部工具截图或保存您的照片副本"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("请注意"), - "disableLinkMessage": m14, + "disableLinkMessage": m16, "disableTwofactor": MessageLookupByLibrary.simpleMessage("禁用双因素认证"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage("正在禁用双因素认证..."), @@ -502,9 +508,9 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("下载"), "downloadFailed": MessageLookupByLibrary.simpleMessage("下載失敗"), "downloading": MessageLookupByLibrary.simpleMessage("正在下载..."), - "dropSupportEmail": m15, - "duplicateFileCountWithStorageSaved": m16, - "duplicateItemsGroup": m17, + "dropSupportEmail": m17, + "duplicateFileCountWithStorageSaved": m18, + "duplicateItemsGroup": m19, "edit": MessageLookupByLibrary.simpleMessage("编辑"), "editLocation": MessageLookupByLibrary.simpleMessage("编辑位置"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("编辑位置"), @@ -513,8 +519,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("对位置的编辑只能在 Ente 内看到"), "eligible": MessageLookupByLibrary.simpleMessage("符合资格"), "email": MessageLookupByLibrary.simpleMessage("电子邮件地址"), - "emailChangedTo": m18, - "emailNoEnteAccount": m19, + "emailChangedTo": m20, + "emailNoEnteAccount": m21, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("电子邮件验证"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("通过电子邮件发送您的日志"), @@ -591,31 +597,29 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("文件已保存到相册"), "fileTypes": MessageLookupByLibrary.simpleMessage("文件类型"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("文件类型和名称"), - "filesBackedUpFromDevice": m20, - "filesBackedUpInAlbum": m21, + "filesBackedUpFromDevice": m22, + "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("上下翻转"), "forYourMemories": MessageLookupByLibrary.simpleMessage("为您的回忆"), "forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("已领取的免费存储"), - "freeStorageOnReferralSuccess": m22, - "freeStorageSpace": m23, + "freeStorageOnReferralSuccess": m24, + "freeStorageSpace": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("可用的免费存储"), "freeTrial": MessageLookupByLibrary.simpleMessage("免费试用"), - "freeTrialValidTill": m24, - "freeUpAccessPostDelete": m25, - "freeUpAmount": m26, + "freeTrialValidTill": m26, + "freeUpAccessPostDelete": m27, + "freeUpAmount": m28, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("释放设备空间"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("释放空间"), - "freeUpSpaceSaving": m27, + "freeUpSpaceSaving": m29, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage("在图库中显示最多1000个回忆"), "general": MessageLookupByLibrary.simpleMessage("通用"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("正在生成加密密钥..."), - "genericProgress": m28, + "genericProgress": m30, "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": @@ -626,6 +630,7 @@ class MessageLookup extends MessageLookupByLibrary { "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage("您是如何知道Ente的? (可选的)"), + "help": MessageLookupByLibrary.simpleMessage("帮助"), "hidden": MessageLookupByLibrary.simpleMessage("已隐藏"), "hide": MessageLookupByLibrary.simpleMessage("隐藏"), "hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."), @@ -666,7 +671,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), - "itemCount": m29, + "itemCount": m31, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"), "itemsWillBeRemovedFromAlbum": @@ -689,7 +694,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("设备限制"), "linkEnabled": MessageLookupByLibrary.simpleMessage("已启用"), "linkExpired": MessageLookupByLibrary.simpleMessage("已过期"), - "linkExpiresOn": m30, + "linkExpiresOn": m32, "linkExpiry": MessageLookupByLibrary.simpleMessage("链接过期"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("链接已过期"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("永不"), @@ -720,6 +725,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("地点名称"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage("位置标签将在照片的某个半径范围内拍摄的所有照片进行分组"), + "locations": MessageLookupByLibrary.simpleMessage("位置"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("锁定"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage("要启用锁屏,请在系统设置中设置设备密码或屏幕锁定。"), @@ -731,6 +737,8 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("退出登录"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "这将跨日志发送以帮助我们调试您的问题。 请注意,将包含文件名以帮助跟踪特定文件的问题。"), + "longPressAnEmailToVerifyEndToEndEncryption": + MessageLookupByLibrary.simpleMessage("长按电子邮件以验证端到端加密。"), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage("长按一个项目来全屏查看"), "lostDevice": MessageLookupByLibrary.simpleMessage("丢失了设备吗?"), @@ -748,7 +756,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("地图"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), - "memoryCount": m31, + "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("商品"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("移动端, 网页端, 桌面端"), @@ -757,10 +765,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("修改您的查询,或尝试搜索"), "moments": MessageLookupByLibrary.simpleMessage("瞬间"), "monthly": MessageLookupByLibrary.simpleMessage("每月"), - "moveItem": m32, + "moveItem": m34, "moveToAlbum": MessageLookupByLibrary.simpleMessage("移动到相册"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("移至隐藏相册"), - "movedSuccessfullyTo": m33, + "movedSuccessfullyTo": m35, "movedToTrash": MessageLookupByLibrary.simpleMessage("已移至回收站"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件移动到相册..."), @@ -822,13 +830,14 @@ class MessageLookup extends MessageLookupByLibrary { "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("密码修改成功"), "passwordLock": MessageLookupByLibrary.simpleMessage("密码锁"), - "passwordStrength": m34, + "passwordStrength": m36, "passwordWarning": MessageLookupByLibrary.simpleMessage( "我们不储存这个密码,所以如果忘记, 我们将无法解密您的数据"), "paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"), "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), - "paymentFailedTalkToProvider": m35, - "paymentFailedWithReason": m36, + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"), + "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"), "pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("使用您的代码的人"), @@ -846,7 +855,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"), "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), - "playStoreFreeTrialValidTill": m37, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -856,10 +865,10 @@ class MessageLookup extends MessageLookupByLibrary { "请用英语联系 support@ente.io ,我们将乐意提供帮助!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("如果问题仍然存在,请联系支持"), - "pleaseEmailUsAt": m38, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("请授予权限"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("请重新登录"), - "pleaseSendTheLogsTo": m39, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("请重试"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("请验证您输入的代码"), @@ -885,7 +894,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("提升工单"), "rateTheApp": MessageLookupByLibrary.simpleMessage("为此应用评分"), "rateUs": MessageLookupByLibrary.simpleMessage("给我们评分"), - "rateUsOnStore": m40, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("恢复"), "recoverAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoverButton": MessageLookupByLibrary.simpleMessage("恢复"), @@ -910,7 +919,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), - "referralStep3": m41, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("推荐人"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("推荐已暂停"), @@ -929,7 +938,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeFromFavorite": MessageLookupByLibrary.simpleMessage("从收藏中移除"), "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), - "removeParticipantBody": m42, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage("您要删除的某些项目是由其他人添加的,您将无法访问它们"), @@ -940,7 +949,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("重命名相册"), "renameFile": MessageLookupByLibrary.simpleMessage("重命名文件"), "renewSubscription": MessageLookupByLibrary.simpleMessage("续费订阅"), - "renewsOn": m43, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("报告错误"), "reportBug": MessageLookupByLibrary.simpleMessage("报告错误"), "resendEmail": MessageLookupByLibrary.simpleMessage("重新发送电子邮件"), @@ -987,7 +996,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("在照片的一定半径内拍摄的几组照片"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), - "searchResultCount": m44, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("安全"), "selectALocation": MessageLookupByLibrary.simpleMessage("选择一个位置"), "selectALocationFirst": @@ -1007,8 +1016,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("所选文件夹将被加密和备份"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"), - "selectedPhotos": m45, - "selectedPhotosWithYours": m46, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("发送"), "sendEmail": MessageLookupByLibrary.simpleMessage("发送电子邮件"), "sendInvite": MessageLookupByLibrary.simpleMessage("发送邀请"), @@ -1027,16 +1036,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开相册并点击右上角的分享按钮进行分享"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("立即分享相册"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), - "shareMyVerificationID": m47, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("仅与您想要的人分享"), - "shareTextConfirmOthersVerificationID": m48, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "下载 ente,以便我们轻松分享原始质量的照片和视频\n\nhttps://ente.io"), - "shareTextReferralCode": m49, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("与非ente 用户分享"), - "shareWithPeopleSectionTitle": m50, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("分享您的第一个相册"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1047,7 +1056,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新共享的照片"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("当有人将照片添加到您所属的共享相册时收到通知"), - "sharedWith": m51, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("与我共享"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("已与您共享"), "sharing": MessageLookupByLibrary.simpleMessage("正在分享..."), @@ -1059,11 +1068,11 @@ class MessageLookup extends MessageLookupByLibrary { "signOutOtherDevices": MessageLookupByLibrary.simpleMessage("登出其他设备"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "我同意 服务条款隐私政策"), - "singleFileDeleteFromDevice": m52, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), - "singleFileInBothLocalAndRemote": m53, - "singleFileInRemoteOnly": m54, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": @@ -1094,12 +1103,12 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("存储空间"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("家庭"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), - "storageInGB": m55, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("已超出存储限制"), - "storageUsageInfo": m56, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("强"), - "subAlreadyLinkedErrMessage": m57, - "subWillBeCancelledOn": m58, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("订阅"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage("您的订阅似乎已过期。请订阅以启用分享。"), @@ -1112,7 +1121,7 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("已成功取消隐藏"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("建议新功能"), "support": MessageLookupByLibrary.simpleMessage("支持"), - "syncProgress": m59, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), @@ -1135,7 +1144,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("主题"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("这些项目将从您的设备中删除。"), - "theyAlsoGetXGb": m60, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("他们将从所有相册中删除。"), "thisActionCannotBeUndone": @@ -1149,7 +1158,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这个邮箱地址已经被使用"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("此图像没有Exif 数据"), - "thisIsPersonVerificationId": m61, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("这是您的验证 ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1163,7 +1172,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("总计"), "totalSize": MessageLookupByLibrary.simpleMessage("总大小"), "trash": MessageLookupByLibrary.simpleMessage("回收站"), - "trashDaysLeft": m62, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("请再试一次"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage("打开备份以自动上传添加到此设备文件夹的文件。"), @@ -1206,13 +1215,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"), "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"), - "validTill": m63, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("验证失败,请重试"), "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"), "verify": MessageLookupByLibrary.simpleMessage("验证"), "verifyEmail": MessageLookupByLibrary.simpleMessage("验证电子邮件"), - "verifyEmailID": m64, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("验证"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("验证通行密钥"), "verifyPassword": MessageLookupByLibrary.simpleMessage("验证密码"), @@ -1236,11 +1245,11 @@ class MessageLookup extends MessageLookupByLibrary { "weAreOpenSource": MessageLookupByLibrary.simpleMessage("我们是开源的 !"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage("我们不支持编辑您尚未拥有的照片和相册"), - "weHaveSendEmailTo": m65, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("弱"), "welcomeBack": MessageLookupByLibrary.simpleMessage("欢迎回来!"), "yearly": MessageLookupByLibrary.simpleMessage("每年"), - "yearsAgo": m66, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("是"), "yesCancel": MessageLookupByLibrary.simpleMessage("是的,取消"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("是的,转换为查看者"), @@ -1266,7 +1275,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("莫开玩笑,您不能与自己分享"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("您没有任何存档的项目。"), - "youHaveSuccessfullyFreedUp": m67, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("您的账户已删除"), "yourMap": MessageLookupByLibrary.simpleMessage("您的地图"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index f24c440b0..27ee28aaf 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -2232,6 +2232,16 @@ class S { ); } + /// `Help` + String get help { + return Intl.message( + 'Help', + name: 'help', + desc: '', + args: [], + ); + } + /// `Oops, something went wrong` String get oopsSomethingWentWrong { return Intl.message( @@ -4465,13 +4475,13 @@ class S { ); } - /// `Unfortunately your payment failed due to {reason}` - String paymentFailedWithReason(Object reason) { + /// `Unfortunately your payment failed. Please contact support and we'll help you out!` + String get paymentFailedMessage { return Intl.message( - 'Unfortunately your payment failed due to $reason', - name: 'paymentFailedWithReason', + 'Unfortunately your payment failed. Please contact support and we\'ll help you out!', + name: 'paymentFailedMessage', desc: '', - args: [reason], + args: [], ); } @@ -8457,6 +8467,42 @@ class S { args: [], ); } + + /// `{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}` + String addViewers(num count) { + return Intl.plural( + count, + zero: 'Add viewer', + one: 'Add viewer', + other: 'Add viewers', + name: 'addViewers', + desc: '', + args: [count], + ); + } + + /// `{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}` + String addCollaborators(num count) { + return Intl.plural( + count, + zero: 'Add collaborator', + one: 'Add collaborator', + other: 'Add collaborators', + name: 'addCollaborators', + desc: '', + args: [count], + ); + } + + /// `Long press an email to verify end to end encryption.` + String get longPressAnEmailToVerifyEndToEndEncryption { + return Intl.message( + 'Long press an email to verify end to end encryption.', + name: 'longPressAnEmailToVerifyEndToEndEncryption', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 66ed18d55..b552b1052 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -14,6 +14,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index 9604c03d8..460489037 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -1201,5 +1201,8 @@ "deviceCodeHint": "Code eingeben", "joinDiscord": "Discord beitreten", "locations": "Orte", - "descriptions": "Beschreibungen" + "descriptions": "Beschreibungen", + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 0460ddea8..d1e6d61aa 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -304,6 +304,7 @@ } }, "faq": "FAQ", + "help": "Help", "oopsSomethingWentWrong": "Oops, something went wrong", "peopleUsingYourCode": "People using your code", "eligible": "eligible", @@ -640,7 +641,7 @@ "thankYou": "Thank you", "failedToVerifyPaymentStatus": "Failed to verify payment status", "pleaseWaitForSometimeBeforeRetrying": "Please wait for sometime before retrying", - "paymentFailedWithReason": "Unfortunately your payment failed due to {reason}", + "paymentFailedMessage": "Unfortunately your payment failed. Please contact support and we'll help you out!", "youAreOnAFamilyPlan": "You are on a family plan!", "contactFamilyAdmin": "Please contact {familyAdminEmail} to manage your subscription", "leaveFamily": "Leave family", @@ -1202,5 +1203,8 @@ "locations": "Locations", "descriptions": "Descriptions", "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "findPeopleByName": "Find people quickly by searching by name", + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index e44176c75..fcdff5ccf 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -976,6 +976,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index caede7dee..d24e11827 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -1157,6 +1157,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index d302fec22..ef8eeda34 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -1119,6 +1119,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ko.arb b/mobile/lib/l10n/intl_ko.arb index 66ed18d55..b552b1052 100644 --- a/mobile/lib/l10n/intl_ko.arb +++ b/mobile/lib/l10n/intl_ko.arb @@ -14,6 +14,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index d21326313..43ca7ee5d 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1195,6 +1195,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index efad4e47e..b4ba1d96d 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -28,6 +28,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index ac1b6dce7..8dd33fda6 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -115,6 +115,7 @@ "joinDiscord": "Join Discord", "locations": "Locations", "descriptions": "Descriptions", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", + "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 9a842fbe9..cfb2bef29 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -304,6 +304,7 @@ } }, "faq": "Perguntas frequentes", + "help": "Ajuda", "oopsSomethingWentWrong": "Ops! Algo deu errado", "peopleUsingYourCode": "Pessoas que usam seu código", "eligible": "elegível", @@ -338,11 +339,11 @@ "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", "sharing": "Compartilhando...", "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivo", + "archive": "Arquivado", "createAlbumActionHint": "Pressione e segure para selecionar fotos e clique em + para criar um álbum", "importing": "Importando....", "failedToLoadAlbums": "Falha ao carregar álbuns", - "hidden": "Escondido", + "hidden": "Oculto", "authToViewYourHiddenFiles": "Autentique-se para visualizar seus arquivos ocultos", "trash": "Lixeira", "uncategorized": "Sem categoria", @@ -545,7 +546,7 @@ "yourStorageDetailsCouldNotBeFetched": "Seus detalhes de armazenamento não puderam ser obtidos", "reportABug": "Reportar um problema", "reportBug": "Reportar um problema", - "suggestFeatures": "Sugerir funcionalidades", + "suggestFeatures": "Sugerir recurso", "support": "Suporte", "theme": "Tema", "lightTheme": "Claro", @@ -640,7 +641,7 @@ "thankYou": "Obrigado", "failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento", "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente", - "paymentFailedWithReason": "Infelizmente o seu pagamento falhou devido a {reason}", + "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 da família", @@ -839,7 +840,7 @@ "pressAndHoldToPlayVideo": "Pressione e segure para reproduzir o vídeo", "pressAndHoldToPlayVideoDetailed": "Pressione e segure na imagem para reproduzir o vídeo", "downloadFailed": "Falha ao baixar", - "deduplicateFiles": "Arquivos Deduplicados", + "deduplicateFiles": "Arquivos duplicados", "deselectAll": "Desmarcar todos", "reviewDeduplicateItems": "Por favor, reveja e exclua os itens que você acredita serem duplicados.", "clubByCaptureTime": "Agrupar por tempo de captura", @@ -1200,5 +1201,8 @@ "joinDiscord": "Junte-se ao Discord", "findPeopleByName": "Find people quickly by searching by name", "locations": "Locais", - "descriptions": "Descrições" + "descriptions": "Descrições", + "addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}", + "addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index 64b3e957f..982ff40f7 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -304,6 +304,7 @@ } }, "faq": "常见问题", + "help": "帮助", "oopsSomethingWentWrong": "哎呀,似乎出了点问题", "peopleUsingYourCode": "使用您的代码的人", "eligible": "符合资格", @@ -640,7 +641,7 @@ "thankYou": "非常感谢您", "failedToVerifyPaymentStatus": "验证支付状态失败", "pleaseWaitForSometimeBeforeRetrying": "请稍等片刻后再重试", - "paymentFailedWithReason": "很抱歉,您的支付因 {reason} 而失败", + "paymentFailedMessage": "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!", "youAreOnAFamilyPlan": "你在一个家庭计划中!", "contactFamilyAdmin": "请联系 {familyAdminEmail} 来管理您的订阅", "leaveFamily": "离开家庭计划", @@ -1198,6 +1199,9 @@ "castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。", "deviceCodeHint": "输入代码", "joinDiscord": "加入 Discord", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name" + "locations": "位置", + "descriptions": "描述", + "addViewers": "{count, plural, zero {添加查看者} one {添加查看者} other {添加查看者}}", + "addCollaborators": "{count, plural, zero {添加协作者} one {添加协作者} other {添加协作者}}", + "longPressAnEmailToVerifyEndToEndEncryption": "长按电子邮件以验证端到端加密。" } \ No newline at end of file diff --git a/mobile/lib/models/subscription.dart b/mobile/lib/models/subscription.dart index 477942421..51fca19e3 100644 --- a/mobile/lib/models/subscription.dart +++ b/mobile/lib/models/subscription.dart @@ -30,6 +30,19 @@ class Subscription { return expiryTime > DateTime.now().microsecondsSinceEpoch; } + bool isCancelled() { + return attributes?.isCancelled ?? false; + } + + bool isPastDue() { + return !isCancelled() && + expiryTime < DateTime.now().microsecondsSinceEpoch && + expiryTime >= + DateTime.now() + .subtract(const Duration(days: 30)) + .microsecondsSinceEpoch; + } + bool isYearlyPlan() { return 'year' == period; } diff --git a/mobile/lib/services/billing_service.dart b/mobile/lib/services/billing_service.dart index e9485f50c..1d67531cc 100644 --- a/mobile/lib/services/billing_service.dart +++ b/mobile/lib/services/billing_service.dart @@ -16,7 +16,11 @@ import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/web_page.dart'; import 'package:photos/utils/dialog_util.dart'; -const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect"; +const kWebPaymentRedirectUrl = String.fromEnvironment( + "web-payment-redirect", + defaultValue: "https://payments.ente.io/frameRedirect", +); + const kWebPaymentBaseEndpoint = String.fromEnvironment( "web-payment", defaultValue: "https://payments.ente.io", diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index c8082b448..33ef5d2bb 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -145,13 +145,7 @@ class HomeWidgetService { } Future countHomeWidgets() async { - return await hw.HomeWidget.getWidgetCount( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ) ?? - 0; + return (await hw.HomeWidget.getInstalledWidgets()).length; } Future clearHomeWidget() async { diff --git a/mobile/lib/services/ignored_files_service.dart b/mobile/lib/services/ignored_files_service.dart index 759acd157..d4a4af9f9 100644 --- a/mobile/lib/services/ignored_files_service.dart +++ b/mobile/lib/services/ignored_files_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:logging/logging.dart'; +import "package:photos/core/errors.dart"; import 'package:photos/db/ignored_files_db.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/models/ignored_file.dart'; @@ -47,7 +48,10 @@ class IgnoredFilesService { return false; } - String? getUploadSkipReason(Map idToReasonMap, EnteFile file) { + String? getUploadSkipReason( + Map idToReasonMap, + EnteFile file, + ) { final id = _getIgnoreID(file.localID, file.deviceFolder, file.title); if (id != null && id.isNotEmpty) { return idToReasonMap[id]; @@ -100,6 +104,12 @@ class IgnoredFilesService { for (IgnoredFile iFile in dbResult) { final id = _idForIgnoredFile(iFile); if (id != null) { + if (Platform.isIOS && + iFile.reason == InvalidReason.sourceFileMissing.name) { + // ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed, + // but the file might be available later + continue; + } result[id] = iFile.reason; } } diff --git a/mobile/lib/services/local_file_update_service.dart b/mobile/lib/services/local_file_update_service.dart index 591cade67..e00ac6c45 100644 --- a/mobile/lib/services/local_file_update_service.dart +++ b/mobile/lib/services/local_file_update_service.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'dart:io'; import 'package:logging/logging.dart'; +import "package:photo_manager/photo_manager.dart"; import "package:photos/core/configuration.dart"; import 'package:photos/core/errors.dart'; import 'package:photos/db/file_updation_db.dart'; @@ -25,6 +26,10 @@ class LocalFileUpdateService { late Logger _logger; final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check'; final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check'; + final String _androidMissingGPSImportDone = + 'fm_android_missing_gps_import_done'; + final String _androidMissingGPSCheckDone = + 'fm_android_missing_gps_check_done'; static int twoHundredKb = 200 * 1024; final List _oldMigrationKeys = [ 'fm_badCreationTime', @@ -63,6 +68,9 @@ class LocalFileUpdateService { if (!Platform.isAndroid) { await _handleLivePhotosSizedCheck(); } + if (Platform.isAndroid) { + await _androidMissingGPSCheck(); + } } catch (e, s) { _logger.severe('failed to perform migration', e, s); } finally { @@ -385,6 +393,131 @@ class LocalFileUpdateService { await _prefs.setBool(_doneLivePhotoImport, true); } + //#region Android Missing GPS specific methods ### + + Future _androidMissingGPSCheck() async { + if (_prefs.containsKey(_androidMissingGPSCheckDone)) { + return; + } + await _importAndroidBadGPSCandidate(); + // singleRunLimit indicates number of files to check during single + // invocation of this method. The limit act as a crude way to limit the + // resource consumed by the method + const int singleRunLimit = 500; + final localIDsToProcess = + await _fileUpdationDB.getLocalIDsForPotentialReUpload( + singleRunLimit, + FileUpdationDB.androidMissingGPS, + ); + if (localIDsToProcess.isNotEmpty) { + final chunksOf50 = localIDsToProcess.chunks(50); + for (final chunk in chunksOf50) { + final sTime = DateTime.now().microsecondsSinceEpoch; + final List futures = []; + final chunkOf10 = chunk.chunks(10); + for (final smallChunk in chunkOf10) { + futures.add(_checkForMissingGPS(smallChunk)); + } + await Future.wait(futures); + final eTime = DateTime.now().microsecondsSinceEpoch; + final d = Duration(microseconds: eTime - sTime); + _logger.info( + 'Performed missing GPS Location check for ${chunk.length} files ' + 'completed in ${d.inSeconds.toString()} secs', + ); + } + } else { + _logger.info('Completed android missing GPS check'); + await _prefs.setBool(_androidMissingGPSCheckDone, true); + } + } + + Future _checkForMissingGPS(List localIDs) async { + try { + final List localFiles = + await FilesDB.instance.getLocalFiles(localIDs); + final ownerID = Configuration.instance.getUserID()!; + final Set localIDsWithFile = {}; + final Set reuploadCandidate = {}; + final Set processedIDs = {}; + for (EnteFile file in localFiles) { + if (file.localID == null) continue; + // ignore files that are not uploaded or have different owner + if (!file.isUploaded || file.ownerID! != ownerID) { + processedIDs.add(file.localID!); + } + if (file.hasLocation) { + processedIDs.add(file.localID!); + } + } + for (EnteFile enteFile in localFiles) { + try { + if (enteFile.localID == null || + processedIDs.contains(enteFile.localID!)) { + continue; + } + + final localID = enteFile.localID!; + localIDsWithFile.add(localID); + final AssetEntity? entity = await AssetEntity.fromId(localID); + if (entity == null) { + processedIDs.add(localID); + } else { + final latLng = await entity.latlngAsync(); + if ((latLng.longitude ?? 0) == 0 || (latLng.latitude ?? 0) == 0) { + processedIDs.add(localID); + } else { + reuploadCandidate.add(localID); + processedIDs.add(localID); + } + } + } catch (e, s) { + processedIDs.add(enteFile.localID!); + _logger.severe('lat/long check file ${enteFile.toString()}', e, s); + } + } + for (String id in localIDs) { + // if the file with given localID doesn't exist, consider it as done. + if (!localIDsWithFile.contains(id)) { + processedIDs.add(id); + } + } + await FileUpdationDB.instance.insertMultiple( + reuploadCandidate.toList(), + FileUpdationDB.modificationTimeUpdated, + ); + await FileUpdationDB.instance.deleteByLocalIDs( + processedIDs.toList(), + FileUpdationDB.androidMissingGPS, + ); + } catch (e, s) { + _logger.severe('error while checking missing GPS', e, s); + } + } + + Future _importAndroidBadGPSCandidate() async { + if (_prefs.containsKey(_androidMissingGPSImportDone)) { + return; + } + final sTime = DateTime.now().microsecondsSinceEpoch; + _logger.info('importing files without missing GPS'); + final int ownerID = Configuration.instance.getUserID()!; + final fileLocalIDs = + await FilesDB.instance.getLocalFilesBackedUpWithoutLocation(ownerID); + await _fileUpdationDB.insertMultiple( + fileLocalIDs, + FileUpdationDB.androidMissingGPS, + ); + final eTime = DateTime.now().microsecondsSinceEpoch; + final d = Duration(microseconds: eTime - sTime); + _logger.info( + 'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds', + ); + await _prefs.setBool(_androidMissingGPSImportDone, true); + } + + //#endregion Android Missing GPS specific methods ### + Future getUploadData(EnteFile file) async { final mediaUploadData = await getUploadDataFromEnteFile(file); // delete the file from app's internal cache if it was copied to app diff --git a/mobile/lib/services/local_sync_service.dart b/mobile/lib/services/local_sync_service.dart index b480661cb..93b3c9437 100644 --- a/mobile/lib/services/local_sync_service.dart +++ b/mobile/lib/services/local_sync_service.dart @@ -20,6 +20,7 @@ import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/ignored_files_service.dart"; import 'package:photos/services/local/local_sync_util.dart'; import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/photo_manager_util.dart"; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; import 'package:tuple/tuple.dart'; @@ -61,7 +62,7 @@ class LocalSyncService { return; } if (Platform.isAndroid && AppLifecycleService.instance.isForeground) { - final permissionState = await PhotoManager.requestPermissionExtend(); + final permissionState = await requestPhotoMangerPermissions(); if (permissionState != PermissionState.authorized) { _logger.severe( "sync requested with invalid permission", @@ -213,6 +214,11 @@ class LocalSyncService { _logger.warning('Invalid file received for ignoring: $file'); return; } + if (Platform.isIOS && error.reason == InvalidReason.sourceFileMissing) { + // ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed, + // but the file might be available later + return; + } final ignored = IgnoredFile( file.localID, file.title, diff --git a/mobile/lib/services/machine_learning/machine_learning_controller.dart b/mobile/lib/services/machine_learning/machine_learning_controller.dart index a31da95b1..145670f2c 100644 --- a/mobile/lib/services/machine_learning/machine_learning_controller.dart +++ b/mobile/lib/services/machine_learning/machine_learning_controller.dart @@ -17,8 +17,7 @@ class MachineLearningController { static const kMaximumTemperature = 42; // 42 degree celsius static const kMinimumBatteryLevel = 20; // 20% - static const kInitialInteractionTimeout = Duration(seconds: 10); - static const kDefaultInteractionTimeout = Duration(seconds: 5); + static const kDefaultInteractionTimeout = Duration(seconds: 15); static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; bool _isDeviceHealthy = true; @@ -28,7 +27,7 @@ class MachineLearningController { void init() { if (Platform.isAndroid) { - _startInteractionTimer(timeout: kInitialInteractionTimeout); + _startInteractionTimer(); BatteryInfoPlugin() .androidBatteryInfoStream .listen((AndroidBatteryInfo? batteryInfo) { diff --git a/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart b/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart index 35616b1b1..3abd57db7 100644 --- a/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart +++ b/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart @@ -6,7 +6,6 @@ import "package:logging/logging.dart"; import "package:photos/core/errors.dart"; import "package:photos/core/event_bus.dart"; -import "package:photos/core/network/network.dart"; import "package:photos/events/event.dart"; import "package:photos/services/remote_assets_service.dart"; @@ -17,9 +16,9 @@ abstract class MLFramework { static final _logger = Logger("MLFramework"); final bool shouldDownloadOverMobileData; + final _initializationCompleter = Completer(); InitializationState _state = InitializationState.notInitialized; - final _initializationCompleter = Completer(); MLFramework(this.shouldDownloadOverMobileData) { Connectivity() @@ -65,6 +64,7 @@ abstract class MLFramework { /// instead of a CDN. Future init() async { try { + _initState = InitializationState.initializing; await Future.wait([_initImageModel(), _initTextModel()]); } catch (e, s) { _logger.warning(e, s); @@ -102,41 +102,32 @@ abstract class MLFramework { if (!kImageEncoderEnabled) { return; } - _initState = InitializationState.initializingImageModel; - final imageModel = - await RemoteAssetsService.instance.getAsset(getImageModelRemotePath()); + final imageModel = await _getModel(getImageModelRemotePath()); await loadImageModel(imageModel.path); - _initState = InitializationState.initializedImageModel; } Future _initTextModel() async { - _initState = InitializationState.initializingTextModel; - final textModel = - await RemoteAssetsService.instance.getAsset(getTextModelRemotePath()); + final textModel = await _getModel(getTextModelRemotePath()); await loadTextModel(textModel.path); - _initState = InitializationState.initializedTextModel; } - Future _downloadFile( - String url, - String savePath, { + Future _getModel( + String url, { int trialCount = 1, }) async { + if (await RemoteAssetsService.instance.hasAsset(url)) { + return RemoteAssetsService.instance.getAsset(url); + } if (!await _canDownload()) { _initState = InitializationState.waitingForNetwork; throw WiFiUnavailableError(); } - _logger.info("Downloading " + url); - final existingFile = File(savePath); - if (await existingFile.exists()) { - await existingFile.delete(); - } try { - await NetworkClient.instance.getDio().download(url, savePath); + return RemoteAssetsService.instance.getAsset(url); } catch (e, s) { _logger.severe(e, s); if (trialCount < kMaximumRetrials) { - return _downloadFile(url, savePath, trialCount: trialCount + 1); + return _getModel(url, trialCount: trialCount + 1); } else { rethrow; } @@ -159,9 +150,6 @@ class MLFrameworkInitializationUpdateEvent extends Event { enum InitializationState { notInitialized, waitingForNetwork, - initializingImageModel, - initializedImageModel, - initializingTextModel, - initializedTextModel, + initializing, initialized, } diff --git a/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart b/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart index 5b91eaaee..546450770 100644 --- a/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart +++ b/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart @@ -15,9 +15,10 @@ class OnnxImageEncoder { ..setIntraOpNumThreads(1) ..setSessionGraphOptimizationLevel(GraphOptimizationLevel.ortEnableAll); try { - final bytes = await File(args["imageModelPath"]).readAsBytes(); - final session = OrtSession.fromBuffer(bytes, sessionOptions); + final session = + OrtSession.fromFile(File(args["imageModelPath"]), sessionOptions); _logger.info('image model loaded'); + return session.address; } catch (e, s) { _logger.severe(e, s); diff --git a/mobile/lib/services/remote_assets_service.dart b/mobile/lib/services/remote_assets_service.dart index 0e75b983d..251ce6c15 100644 --- a/mobile/lib/services/remote_assets_service.dart +++ b/mobile/lib/services/remote_assets_service.dart @@ -26,6 +26,11 @@ class RemoteAssetsService { } } + Future hasAsset(String remotePath) async { + final path = await _getLocalPath(remotePath); + return File(path).exists(); + } + Future _getLocalPath(String remotePath) async { return (await getApplicationSupportDirectory()).path + "/assets/" + diff --git a/mobile/lib/services/remote_sync_service.dart b/mobile/lib/services/remote_sync_service.dart index 45bf36bab..0144d70e6 100644 --- a/mobile/lib/services/remote_sync_service.dart +++ b/mobile/lib/services/remote_sync_service.dart @@ -170,7 +170,8 @@ class RemoteSyncService { e is NoActiveSubscriptionError || e is WiFiUnavailableError || e is StorageLimitExceededError || - e is SyncStopRequestedError) { + e is SyncStopRequestedError || + e is NoMediaLocationAccessError) { _logger.warning("Error executing remote sync", e, s); rethrow; } else { @@ -555,6 +556,7 @@ class RemoteSyncService { final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length; if (toBeUploaded > 0) { Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload)); + await _uploader.verifyMediaLocationAccess(); await _uploader.checkNetworkForUpload(); // verify if files upload is allowed based on their subscription plan and // storage limit. To avoid creating new endpoint, we are using diff --git a/mobile/lib/services/sync_service.dart b/mobile/lib/services/sync_service.dart index 8de3e3322..057e600df 100644 --- a/mobile/lib/services/sync_service.dart +++ b/mobile/lib/services/sync_service.dart @@ -120,6 +120,14 @@ class SyncService { } on UnauthorizedError { _logger.info("Logging user out"); Bus.instance.fire(TriggerLogoutEvent()); + } on NoMediaLocationAccessError { + _logger.severe("Not uploading due to no media location access"); + Bus.instance.fire( + SyncStatusUpdate( + SyncStatus.error, + error: NoMediaLocationAccessError(), + ), + ); } catch (e) { if (e is DioError) { if (e.type == DioErrorType.connectTimeout || diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 759adaf42..21a2c59bc 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -16,7 +16,7 @@ class UpdateService { static final UpdateService instance = UpdateService._privateConstructor(); static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 15; + static const currentChangeLogVersion = 17; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index 1e3ac0d95..dc28197bd 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -198,6 +198,62 @@ class CollectionActions { return false; } + Future doesEmailHaveAccount( + BuildContext context, + String email, { + bool showProgress = false, + }) async { + ProgressDialog? dialog; + String? publicKey; + if (showProgress) { + dialog = createProgressDialog( + context, + S.of(context).sharing, + isDismissible: true, + ); + await dialog.show(); + } + try { + publicKey = await UserService.instance.getPublicKey(email); + } catch (e) { + await dialog?.hide(); + logger.severe("Failed to get public key", e); + await showGenericErrorDialog(context: context, error: e); + return false; + } + // getPublicKey can return null when no user is associated with given + // email id + if (publicKey == null || publicKey == '') { + // todo: neeraj replace this as per the design where a new screen + // is used for error. Do this change along with handling of network errors + await showDialogWidget( + context: context, + title: S.of(context).inviteToEnte, + icon: Icons.info_outline, + body: S.of(context).emailNoEnteAccount(email), + isDismissible: true, + buttons: [ + ButtonWidget( + buttonType: ButtonType.neutral, + icon: Icons.adaptive.share, + labelText: S.of(context).sendInvite, + isInAlert: true, + onTap: () async { + unawaited( + shareText( + S.of(context).shareTextRecommendUsingEnte, + ), + ); + }, + ), + ], + ); + return false; + } else { + return true; + } + } + // addEmailToCollection returns true if add operation was successful Future addEmailToCollection( BuildContext context, diff --git a/mobile/lib/ui/components/buttons/button_widget.dart b/mobile/lib/ui/components/buttons/button_widget.dart index 2d1b170fe..2e4d2d1dc 100644 --- a/mobile/lib/ui/components/buttons/button_widget.dart +++ b/mobile/lib/ui/components/buttons/button_widget.dart @@ -202,14 +202,14 @@ class _ButtonChildWidgetState extends State { @override void initState() { - _setButtonTheme(); super.initState(); + _setButtonTheme(); } @override void didUpdateWidget(covariant ButtonChildWidget oldWidget) { - _setButtonTheme(); super.didUpdateWidget(oldWidget); + _setButtonTheme(); } @override diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart index 6fb8f0f6d..7f2519a19 100644 --- a/mobile/lib/ui/components/home_header_widget.dart +++ b/mobile/lib/ui/components/home_header_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/settings/backup/backup_folder_selection_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; +import "package:photos/utils/photo_manager_util.dart"; class HomeHeaderWidget extends StatefulWidget { final Widget centerWidget; @@ -48,7 +49,7 @@ class _HomeHeaderWidgetState extends State { onTap: () async { try { final PermissionState state = - await PhotoManager.requestPermissionExtend(); + await requestPhotoMangerPermissions(); await LocalSyncService.instance.onUpdatePermission(state); } on Exception catch (e) { Logger("HomeHeaderWidget").severe( diff --git a/mobile/lib/ui/components/keyboard/keybiard_oveylay.dart b/mobile/lib/ui/components/keyboard/keyboard_oveylay.dart similarity index 100% rename from mobile/lib/ui/components/keyboard/keybiard_oveylay.dart rename to mobile/lib/ui/components/keyboard/keyboard_oveylay.dart diff --git a/mobile/lib/ui/growth/referral_screen.dart b/mobile/lib/ui/growth/referral_screen.dart index 32843c192..570114600 100644 --- a/mobile/lib/ui/growth/referral_screen.dart +++ b/mobile/lib/ui/growth/referral_screen.dart @@ -255,7 +255,7 @@ class ReferralWidget extends StatelessWidget { context, WebPage( S.of(context).faq, - "https://ente.io/faq/general/referral-program", + "https://help.ente.io/photos/features/referral-program/", ), ); }, diff --git a/mobile/lib/ui/home/grant_permissions_widget.dart b/mobile/lib/ui/home/grant_permissions_widget.dart index 29c8e5e1b..ab6aff07b 100644 --- a/mobile/lib/ui/home/grant_permissions_widget.dart +++ b/mobile/lib/ui/home/grant_permissions_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/sync_service.dart'; +import "package:photos/utils/photo_manager_util.dart"; import "package:styled_text/styled_text.dart"; class GrantPermissionsWidget extends StatelessWidget { @@ -91,7 +92,7 @@ class GrantPermissionsWidget extends StatelessWidget { key: const ValueKey("grantPermissionButton"), child: Text(S.of(context).grantPermission), onPressed: () async { - final state = await PhotoManager.requestPermissionExtend(); + final state = await requestPhotoMangerPermissions(); if (state == PermissionState.authorized || state == PermissionState.limited) { await SyncService.instance.onPermissionGranted(state); diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 6ea2510f2..8cf629cd6 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,5 +1,4 @@ import "dart:async"; -import "dart:io"; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; @@ -85,22 +84,14 @@ class _ChangeLogPageState extends State { ButtonWidget( buttonType: ButtonType.trailingIconSecondary, buttonSize: ButtonSize.large, - // labelText: S.of(context).joinDiscord, - labelText: "Why we open sourced", - // icon: Icons.discord_outlined, - icon: Icons.rocket_rounded, + labelText: S.of(context).joinDiscord, + icon: Icons.discord_outlined, iconColor: enteColorScheme.primary500, onTap: () async { - // unawaited( - // launchUrlString( - // "https://discord.com/invite/z2YVKkycX3", - // mode: LaunchMode.externalApplication, - // ), - // ); unawaited( launchUrlString( - "https://ente.io/blog/open-sourcing-our-server/", - mode: LaunchMode.inAppBrowserView, + "https://discord.com/invite/z2YVKkycX3", + mode: LaunchMode.externalApplication, ), ); }, @@ -129,32 +120,16 @@ class _ChangeLogPageState extends State { Widget _getChangeLog() { final scrollController = ScrollController(); final List items = []; - if (Platform.isAndroid) { - items.add( - ChangeLogEntry( - "Home Widget ✨", - 'Introducing our new Android widget! Enjoy your favourite memories directly on your home screen.', - ), - ); - } items.addAll([ ChangeLogEntry( - "Redesigned Discovery Tab", - 'We\'ve given it a fresh new look for improved design and better visual separation between each section.', + "Share an Album to Multiple Contacts at Once", + 'Adding multiple viewers and collaborators just got easier!\n' + '\nYou can now select multiple contacts and add all of them at once.', ), ChangeLogEntry( - "Location Clustering ", - 'Now, see photos automatically organize into clusters around a radius of populated cities.', - ), - ChangeLogEntry( - "Ente is now fully Open Source!", - 'We took the final step in our open source journey.\n\n' - 'Our clients had always been open source. Now, we have released the source code for our servers.', - ), - ChangeLogEntry( - "Bug Fixes", - 'Many a bugs were squashed in this release.\n' - '\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏', + "Bug Fixes and Performance Improvements", + 'Many a bugs were squashed in this release and have improved performance on app start.\n' + '\nIf you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏', ), ]); diff --git a/mobile/lib/ui/payment/payment_web_page.dart b/mobile/lib/ui/payment/payment_web_page.dart index 39f4ea9d0..cbe55f671 100644 --- a/mobile/lib/ui/payment/payment_web_page.dart +++ b/mobile/lib/ui/payment/payment_web_page.dart @@ -5,14 +5,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:logging/logging.dart'; +import "package:photos/core/constants.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/subscription.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/utils/dialog_util.dart'; +import "package:photos/utils/email_util.dart"; class PaymentWebPage extends StatefulWidget { final String? planId; @@ -30,7 +31,6 @@ class _PaymentWebPageState extends State { final UserService userService = UserService.instance; final BillingService billingService = BillingService.instance; final String basePaymentUrl = kWebPaymentBaseEndpoint; - late ProgressDialog _dialog; InAppWebViewController? webView; double progress = 0; Uri? initPaymentUrl; @@ -49,11 +49,6 @@ class _PaymentWebPageState extends State { @override Widget build(BuildContext context) { - _dialog = createProgressDialog( - context, - S.of(context).pleaseWait, - isDismissible: true, - ); if (initPaymentUrl == null) { return const EnteLoadingWidget(); } @@ -93,27 +88,20 @@ class _PaymentWebPageState extends State { return NavigationActionPolicy.ALLOW; }, onConsoleMessage: (controller, consoleMessage) { - _logger.info(consoleMessage); + _logger.info("onConsoleMessage $consoleMessage"); }, onLoadStart: (controller, navigationAction) async { - if (!_dialog.isShowing()) { - await _dialog.show(); - } + _logger.info("onLoadStart $navigationAction"); }, onLoadError: (controller, navigationAction, code, msg) async { - if (_dialog.isShowing()) { - await _dialog.hide(); - } + _logger.severe("onLoadError $navigationAction $code $msg"); }, onLoadHttpError: (controller, navigationAction, code, msg) async { _logger.info("onHttpError with $code and msg = $msg"); }, onLoadStop: (controller, navigationAction) async { - _logger.info("loadStart" + navigationAction.toString()); - if (_dialog.isShowing()) { - await _dialog.hide(); - } + _logger.info("onLoadStop $navigationAction"); }, ), ), @@ -125,7 +113,6 @@ class _PaymentWebPageState extends State { @override void dispose() { - _dialog.hide(); super.dispose(); } @@ -207,12 +194,17 @@ class _PaymentWebPageState extends State { barrierDismissible: false, builder: (context) => AlertDialog( title: Text(S.of(context).paymentFailed), - content: Text(S.of(context).paymentFailedWithReason(reason)), + content: Text(S.of(context).paymentFailedMessage), actions: [ TextButton( - child: Text(S.of(context).ok), - onPressed: () { + child: Text(S.of(context).contactSupport), + onPressed: () async { Navigator.of(context).pop('dialog'); + await sendEmail( + context, + to: supportEmail, + subject: "Billing issue", + ); }, ), ], @@ -224,7 +216,6 @@ class _PaymentWebPageState extends State { // return true if verifySubscription didn't throw any exceptions Future _handlePaymentSuccess(Map queryParams) async { final checkoutSessionID = queryParams['session_id'] ?? ''; - await _dialog.show(); try { // ignore: unused_local_variable final response = await billingService.verifySubscription( @@ -232,7 +223,6 @@ class _PaymentWebPageState extends State { checkoutSessionID, paymentProvider: stripe, ); - await _dialog.hide(); final content = widget.actionType == 'buy' ? S.of(context).yourPurchaseWasSuccessful : S.of(context).yourSubscriptionWasUpdatedSuccessfully; @@ -242,7 +232,6 @@ class _PaymentWebPageState extends State { ); } catch (error) { _logger.severe(error); - await _dialog.hide(); await _showExitPageDialog( title: S.of(context).failedToVerifyPaymentStatus, content: S.of(context).pleaseWaitForSometimeBeforeRetrying, diff --git a/mobile/lib/ui/payment/stripe_subscription_page.dart b/mobile/lib/ui/payment/stripe_subscription_page.dart index 51a80a67e..8306c3c18 100644 --- a/mobile/lib/ui/payment/stripe_subscription_page.dart +++ b/mobile/lib/ui/payment/stripe_subscription_page.dart @@ -81,6 +81,11 @@ class _StripeSubscriptionPageState extends State { userDetails.hasPaidAddon(); _hasActiveSubscription = _currentSubscription!.isValid(); _isStripeSubscriber = _currentSubscription!.paymentProvider == stripe; + + if (_isStripeSubscriber && _currentSubscription!.isPastDue()) { + _redirectToPaymentPortal(); + } + return _filterStripeForUI().then((value) { _hasLoadedData = true; setState(() {}); @@ -254,7 +259,7 @@ class _StripeSubscriptionPageState extends State { singleBorderRadius: 4, alignCaptionedTextToLeft: true, onTap: () async { - _onStripSupportedPaymentDetailsTap(); + _redirectToPaymentPortal(); }, ), ), @@ -295,9 +300,9 @@ class _StripeSubscriptionPageState extends State { ); } - // _onStripSupportedPaymentDetailsTap action allows the user to update + // _redirectToPaymentPortal action allows the user to update // their stripe payment details - void _onStripSupportedPaymentDetailsTap() async { + void _redirectToPaymentPortal() async { final String paymentProvider = _currentSubscription!.paymentProvider; switch (_currentSubscription!.paymentProvider) { case stripe: @@ -331,6 +336,7 @@ class _StripeSubscriptionPageState extends State { await _dialog.show(); try { final String url = await _billingService.getStripeCustomerPortalUrl(); + await _dialog.hide(); await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { @@ -342,7 +348,6 @@ class _StripeSubscriptionPageState extends State { await _dialog.hide(); await showGenericErrorDialog(context: context, error: e); } - await _dialog.hide(); } Widget _stripeRenewOrCancelButton() { diff --git a/mobile/lib/ui/settings/account_section_widget.dart b/mobile/lib/ui/settings/account_section_widget.dart index 8b9c7bbef..ec4cac4f9 100644 --- a/mobile/lib/ui/settings/account_section_widget.dart +++ b/mobile/lib/ui/settings/account_section_widget.dart @@ -150,7 +150,7 @@ class AccountSectionWidget extends StatelessWidget { trailingIconIsMuted: true, onTap: () async { // ignore: unawaited_futures - launchUrlString("https://ente.io/faq/migration/out-of-ente/"); + launchUrlString("https://help.ente.io/photos/migration/export/"); }, ), sectionOptionSpacing, diff --git a/mobile/lib/ui/settings/support_section_widget.dart b/mobile/lib/ui/settings/support_section_widget.dart index aac3b9413..fa730f703 100644 --- a/mobile/lib/ui/settings/support_section_widget.dart +++ b/mobile/lib/ui/settings/support_section_widget.dart @@ -43,8 +43,8 @@ class SupportSectionWidget extends StatelessWidget { ), sectionOptionSpacing, AboutMenuItemWidget( - title: S.of(context).faq, - url: "https://ente.io/faq", + title: S.of(context).help, + url: "https://help.ente.io", ), sectionOptionSpacing, MenuItemWidget( diff --git a/mobile/lib/ui/sharing/add_participant_page.dart b/mobile/lib/ui/sharing/add_participant_page.dart new file mode 100644 index 000000000..91378b3a5 --- /dev/null +++ b/mobile/lib/ui/sharing/add_participant_page.dart @@ -0,0 +1,402 @@ +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:photos/core/configuration.dart'; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/api/collection/user.dart"; +import 'package:photos/models/collection/collection.dart'; +import 'package:photos/services/collections_service.dart'; +import "package:photos/services/user_service.dart"; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import 'package:photos/ui/components/buttons/button_widget.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/divider_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_section_description_widget.dart'; +import 'package:photos/ui/components/menu_section_title.dart'; +import 'package:photos/ui/components/models/button_type.dart'; +import 'package:photos/ui/sharing/user_avator_widget.dart'; +import "package:photos/ui/sharing/verify_identity_dialog.dart"; +import "package:photos/utils/toast_util.dart"; + +class AddParticipantPage extends StatefulWidget { + final Collection collection; + final bool isAddingViewer; + + const AddParticipantPage(this.collection, this.isAddingViewer, {super.key}); + + @override + State createState() => _AddParticipantPage(); +} + +class _AddParticipantPage extends State { + final _selectedEmails = {}; + String _newEmail = ''; + bool _emailIsValid = false; + bool isKeypadOpen = false; + late CollectionActions collectionActions; + late List _suggestedUsers; + + // Focus nodes are necessary + final textFieldFocusNode = FocusNode(); + final _textController = TextEditingController(); + + @override + void initState() { + super.initState(); + collectionActions = CollectionActions(CollectionsService.instance); + _suggestedUsers = _getSuggestedUser(); + } + + @override + void dispose() { + _textController.dispose(); + textFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final filterSuggestedUsers = _suggestedUsers + .where( + (element) => element.email.toLowerCase().contains( + _textController.text.trim().toLowerCase(), + ), + ) + .toList(); + isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; + final enteTextTheme = getEnteTextTheme(context); + final enteColorScheme = getEnteColorScheme(context); + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + appBar: AppBar( + title: Text( + widget.isAddingViewer + ? S.of(context).addViewer + : S.of(context).addCollaborator, + ), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + S.of(context).addANewEmail, + style: enteTextTheme.small + .copyWith(color: enteColorScheme.textMuted), + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _enterEmailField(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + filterSuggestedUsers.isNotEmpty + ? MenuSectionTitle( + title: S.of(context).orPickAnExistingOne, + ) + : const SizedBox.shrink(), + Expanded( + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + if (index >= filterSuggestedUsers.length) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + filterSuggestedUsers.isNotEmpty + ? MenuSectionDescriptionWidget( + content: S + .of(context) + .longPressAnEmailToVerifyEndToEndEncryption, + ) + : const SizedBox.shrink(), + widget.isAddingViewer + ? const SizedBox.shrink() + : MenuSectionDescriptionWidget( + content: S + .of(context) + .collaboratorsCanAddPhotosAndVideosToTheSharedAlbum, + ), + ], + ), + ); + } + final currentUser = filterSuggestedUsers[index]; + return Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: currentUser.email, + ), + leadingIconSize: 24.0, + leadingIconWidget: UserAvatarWidget( + currentUser, + type: AvatarType.mini, + ), + menuItemColor: + getEnteColorScheme(context).fillFaint, + pressedColor: + getEnteColorScheme(context).fillFaint, + trailingIcon: + (_selectedEmails.contains(currentUser.email)) + ? Icons.check + : null, + onTap: () async { + textFieldFocusNode.unfocus(); + if (_selectedEmails + .contains(currentUser.email)) { + _selectedEmails.remove(currentUser.email); + } else { + _selectedEmails.add(currentUser.email); + } + + setState(() => {}); + // showShortToast(context, "yet to implement"); + }, + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) { + return VerifyIdentifyDialog( + self: false, + email: currentUser.email, + ); + }, + ); + }, + isTopBorderRadiusRemoved: index > 0, + isBottomBorderRadiusRemoved: + index < (filterSuggestedUsers.length - 1), + ), + (index == (filterSuggestedUsers.length - 1)) + ? const SizedBox.shrink() + : DividerWidget( + dividerType: DividerType.menu, + bgColor: + getEnteColorScheme(context).fillFaint, + ), + ], + ); + }, + itemCount: filterSuggestedUsers.length + 1, + ), + ), + ], + ), + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 16, + right: 16, + ), + child: Column( + 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, + "Added $noOfSuccessfullAdds ${widget.isAddingViewer ? "viewers" : "collaborators"}", + ); + + if (!results.any((e) => e == false) && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + const SizedBox(height: 12), + ], + ), + ), + ), + ], + ), + ); + } + + void clearFocus() { + _textController.clear(); + _newEmail = _textController.text; + _emailIsValid = false; + textFieldFocusNode.unfocus(); + setState(() => {}); + } + + Widget _enterEmailField() { + return Row( + children: [ + Expanded( + child: TextFormField( + controller: _textController, + focusNode: textFieldFocusNode, + style: getEnteTextTheme(context).body, + autofillHints: const [AutofillHints.email], + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + borderSide: + BorderSide(color: getEnteColorScheme(context).strokeMuted), + ), + fillColor: getEnteColorScheme(context).fillFaint, + filled: true, + hintText: S.of(context).enterEmail, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(4), + ), + prefixIcon: Icon( + Icons.email_outlined, + color: getEnteColorScheme(context).strokeMuted, + ), + suffixIcon: _newEmail == '' + ? null + : IconButton( + onPressed: clearFocus, + icon: Icon( + Icons.cancel, + color: getEnteColorScheme(context).strokeMuted, + ), + ), + ), + onChanged: (value) { + _newEmail = value.trim(); + _emailIsValid = EmailValidator.validate(_newEmail); + setState(() {}); + }, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + //initialValue: _email, + textInputAction: TextInputAction.next, + ), + ), + const SizedBox(width: 8), + ButtonWidget( + buttonType: ButtonType.secondary, + buttonSize: ButtonSize.small, + labelText: "Add", + isDisabled: !_emailIsValid, + onTap: () async { + if (_emailIsValid) { + final result = await collectionActions.doesEmailHaveAccount( + context, + _newEmail, + ); + if (result && mounted) { + setState(() { + for (var suggestedUser in _suggestedUsers) { + if (suggestedUser.email == _newEmail) { + _selectedEmails.add(suggestedUser.email); + clearFocus(); + + return; + } + } + _suggestedUsers.insert(0, User(email: _newEmail)); + _selectedEmails.add(_newEmail); + clearFocus(); + }); + } + } + }, + ), + ], + ); + } + + List _getSuggestedUser() { + final List suggestedUsers = []; + final Set existingEmails = {}; + final int ownerID = Configuration.instance.getUserID()!; + existingEmails.add(Configuration.instance.getEmail()!); + for (final User? u in widget.collection.sharees ?? []) { + if (u != null && u.id != null && u.email.isNotEmpty) { + existingEmails.add(u.email); + } + } + for (final c in CollectionsService.instance.getActiveCollections()) { + if (c.owner?.id == ownerID) { + for (final User? u in c.sharees ?? []) { + if (u != null && + u.id != null && + u.email.isNotEmpty && + !existingEmails.contains(u.email)) { + existingEmails.add(u.email); + suggestedUsers.add(u); + } + } + } else if (c.owner != null && + c.owner!.id != null && + c.owner!.email.isNotEmpty && + !existingEmails.contains(c.owner!.email)) { + existingEmails.add(c.owner!.email); + suggestedUsers.add(c.owner!); + } + } + final cachedUserDetails = UserService.instance.getCachedUserDetails(); + if (cachedUserDetails != null && + (cachedUserDetails.familyData?.members?.isNotEmpty ?? false)) { + for (final member in cachedUserDetails.familyData!.members!) { + if (!existingEmails.contains(member.email)) { + existingEmails.add(member.email); + suggestedUsers.add(User(email: member.email)); + } + } + } + if (_textController.text.trim().isNotEmpty) { + suggestedUsers.removeWhere( + (element) => !element.email + .toLowerCase() + .contains(_textController.text.trim().toLowerCase()), + ); + } + suggestedUsers.sort((a, b) => a.email.compareTo(b.email)); + + return suggestedUsers; + } +} diff --git a/mobile/lib/ui/sharing/add_partipant_page.dart b/mobile/lib/ui/sharing/add_partipant_page.dart deleted file mode 100644 index cf828e1fe..000000000 --- a/mobile/lib/ui/sharing/add_partipant_page.dart +++ /dev/null @@ -1,361 +0,0 @@ -import 'package:email_validator/email_validator.dart'; -import 'package:flutter/material.dart'; -import 'package:photos/core/configuration.dart'; -import "package:photos/generated/l10n.dart"; -import "package:photos/models/api/collection/user.dart"; -import 'package:photos/models/collection/collection.dart'; -import 'package:photos/services/collections_service.dart'; -import "package:photos/services/user_service.dart"; -import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/ui/components/buttons/button_widget.dart'; -import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; -import 'package:photos/ui/components/menu_section_description_widget.dart'; -import 'package:photos/ui/components/menu_section_title.dart'; -import 'package:photos/ui/components/models/button_type.dart'; -import 'package:photos/ui/sharing/user_avator_widget.dart'; -import "package:photos/ui/sharing/verify_identity_dialog.dart"; -import "package:photos/utils/dialog_util.dart"; - -class AddParticipantPage extends StatefulWidget { - final Collection collection; - final bool isAddingViewer; - - const AddParticipantPage(this.collection, this.isAddingViewer, {super.key}); - - @override - State createState() => _AddParticipantPage(); -} - -class _AddParticipantPage extends State { - String selectedEmail = ''; - String _email = ''; - bool isEmailListEmpty = false; - bool _emailIsValid = false; - bool isKeypadOpen = false; - late CollectionActions collectionActions; - - // Focus nodes are necessary - final textFieldFocusNode = FocusNode(); - final _textController = TextEditingController(); - - @override - void initState() { - collectionActions = CollectionActions(CollectionsService.instance); - super.initState(); - } - - @override - void dispose() { - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; - final enteTextTheme = getEnteTextTheme(context); - final enteColorScheme = getEnteColorScheme(context); - final List suggestedUsers = _getSuggestedUser(); - isEmailListEmpty = suggestedUsers.isEmpty; - return Scaffold( - resizeToAvoidBottomInset: isKeypadOpen, - appBar: AppBar( - title: Text( - widget.isAddingViewer - ? S.of(context).addViewer - : S.of(context).addCollaborator, - ), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - S.of(context).addANewEmail, - style: enteTextTheme.small - .copyWith(color: enteColorScheme.textMuted), - ), - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _getEmailField(), - ), - (isEmailListEmpty && widget.isAddingViewer) - ? const Expanded(child: SizedBox.shrink()) - : Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - !isEmailListEmpty - ? MenuSectionTitle( - title: S.of(context).orPickAnExistingOne, - ) - : const SizedBox.shrink(), - Expanded( - child: ListView.builder( - itemBuilder: (context, index) { - if (index >= suggestedUsers.length) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - ), - child: MenuSectionDescriptionWidget( - content: S - .of(context) - .collaboratorsCanAddPhotosAndVideosToTheSharedAlbum, - ), - ); - } - final currentUser = suggestedUsers[index]; - return Column( - children: [ - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: currentUser.email, - ), - leadingIconSize: 24.0, - leadingIconWidget: UserAvatarWidget( - currentUser, - type: AvatarType.mini, - ), - menuItemColor: - getEnteColorScheme(context).fillFaint, - pressedColor: - getEnteColorScheme(context).fillFaint, - trailingIcon: - (selectedEmail == currentUser.email) - ? Icons.check - : null, - onTap: () async { - textFieldFocusNode.unfocus(); - if (selectedEmail == currentUser.email) { - selectedEmail = ''; - } else { - selectedEmail = currentUser.email; - } - - setState(() => {}); - // showShortToast(context, "yet to implement"); - }, - isTopBorderRadiusRemoved: index > 0, - isBottomBorderRadiusRemoved: - index < (suggestedUsers.length - 1), - ), - (index == (suggestedUsers.length - 1)) - ? const SizedBox.shrink() - : DividerWidget( - dividerType: DividerType.menu, - bgColor: getEnteColorScheme(context) - .fillFaint, - ), - ], - ); - }, - itemCount: suggestedUsers.length + - (widget.isAddingViewer ? 0 : 1), - // physics: const ClampingScrollPhysics(), - ), - ), - ], - ), - ), - ), - SafeArea( - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 16, - right: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 8), - ButtonWidget( - buttonType: ButtonType.primary, - buttonSize: ButtonSize.large, - labelText: widget.isAddingViewer - ? S.of(context).addViewer - : S.of(context).addCollaborator, - isDisabled: (selectedEmail == '' && !_emailIsValid), - onTap: (selectedEmail == '' && !_emailIsValid) - ? null - : () async { - final emailToAdd = - selectedEmail == '' ? _email : selectedEmail; - final result = - await collectionActions.addEmailToCollection( - context, - widget.collection, - emailToAdd, - widget.isAddingViewer - ? CollectionParticipantRole.viewer - : CollectionParticipantRole.collaborator, - ); - if (result && mounted) { - Navigator.of(context).pop(true); - } - }, - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () async { - if ((selectedEmail == '' && !_emailIsValid)) { - await showErrorDialog( - context, - S.of(context).invalidEmailAddress, - S.of(context).enterValidEmail, - ); - return; - } - final emailToAdd = - selectedEmail == '' ? _email : selectedEmail; - // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return VerifyIdentifyDialog( - self: false, - email: emailToAdd, - ); - }, - ); - }, - child: Text( - S.of(context).verifyIDLabel, - textAlign: TextAlign.center, - style: enteTextTheme.smallMuted.copyWith( - decoration: TextDecoration.underline, - ), - ), - ), - const SizedBox(height: 12), - ], - ), - ), - ), - ], - ), - ); - } - - void clearFocus() { - _textController.clear(); - _email = _textController.text; - _emailIsValid = false; - textFieldFocusNode.unfocus(); - setState(() => {}); - } - - Widget _getEmailField() { - return TextFormField( - controller: _textController, - focusNode: textFieldFocusNode, - style: getEnteTextTheme(context).body, - autofillHints: const [AutofillHints.email], - decoration: InputDecoration( - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - borderSide: - BorderSide(color: getEnteColorScheme(context).strokeMuted), - ), - fillColor: getEnteColorScheme(context).fillFaint, - filled: true, - hintText: S.of(context).enterEmail, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(4), - ), - prefixIcon: Icon( - Icons.email_outlined, - color: getEnteColorScheme(context).strokeMuted, - ), - suffixIcon: _email == '' - ? null - : IconButton( - onPressed: clearFocus, - icon: Icon( - Icons.cancel, - color: getEnteColorScheme(context).strokeMuted, - ), - ), - ), - onChanged: (value) { - if (selectedEmail != '') { - selectedEmail = ''; - } - _email = value.trim(); - _emailIsValid = EmailValidator.validate(_email); - setState(() {}); - }, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - //initialValue: _email, - textInputAction: TextInputAction.next, - ); - } - - List _getSuggestedUser() { - final List suggestedUsers = []; - final Set existingEmails = {}; - final int ownerID = Configuration.instance.getUserID()!; - existingEmails.add(Configuration.instance.getEmail()!); - for (final User? u in widget.collection.sharees ?? []) { - if (u != null && u.id != null && u.email.isNotEmpty) { - existingEmails.add(u.email); - } - } - for (final c in CollectionsService.instance.getActiveCollections()) { - if (c.owner?.id == ownerID) { - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && - u.email.isNotEmpty && - !existingEmails.contains(u.email)) { - existingEmails.add(u.email); - suggestedUsers.add(u); - } - } - } else if (c.owner != null && - c.owner!.id != null && - c.owner!.email.isNotEmpty && - !existingEmails.contains(c.owner!.email)) { - existingEmails.add(c.owner!.email); - suggestedUsers.add(c.owner!); - } - } - final cachedUserDetails = UserService.instance.getCachedUserDetails(); - if (cachedUserDetails != null && - (cachedUserDetails.familyData?.members?.isNotEmpty ?? false)) { - for (final member in cachedUserDetails.familyData!.members!) { - if (!existingEmails.contains(member.email)) { - existingEmails.add(member.email); - suggestedUsers.add(User(email: member.email)); - } - } - } - if (_textController.text.trim().isNotEmpty) { - suggestedUsers.removeWhere( - (element) => !element.email - .toLowerCase() - .contains(_textController.text.trim().toLowerCase()), - ); - } - suggestedUsers.sort((a, b) => a.email.compareTo(b.email)); - - return suggestedUsers; - } -} diff --git a/mobile/lib/ui/sharing/album_participants_page.dart b/mobile/lib/ui/sharing/album_participants_page.dart index 494df319f..0c137f300 100644 --- a/mobile/lib/ui/sharing/album_participants_page.dart +++ b/mobile/lib/ui/sharing/album_participants_page.dart @@ -11,7 +11,7 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; -import 'package:photos/ui/sharing/add_partipant_page.dart'; +import "package:photos/ui/sharing/add_participant_page.dart"; import 'package:photos/ui/sharing/manage_album_participant.dart'; import 'package:photos/ui/sharing/user_avator_widget.dart'; import 'package:photos/utils/navigation_util.dart'; diff --git a/mobile/lib/ui/sharing/share_collection_page.dart b/mobile/lib/ui/sharing/share_collection_page.dart index e63a39f3e..8e515fc68 100644 --- a/mobile/lib/ui/sharing/share_collection_page.dart +++ b/mobile/lib/ui/sharing/share_collection_page.dart @@ -13,7 +13,7 @@ import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; -import 'package:photos/ui/sharing/add_partipant_page.dart'; +import "package:photos/ui/sharing/add_participant_page.dart"; import 'package:photos/ui/sharing/album_participants_page.dart'; import "package:photos/ui/sharing/album_share_info_widget.dart"; import "package:photos/ui/sharing/manage_album_participant.dart"; diff --git a/mobile/lib/ui/viewer/file/detail_page.dart b/mobile/lib/ui/viewer/file/detail_page.dart index 779243c27..4368590ea 100644 --- a/mobile/lib/ui/viewer/file/detail_page.dart +++ b/mobile/lib/ui/viewer/file/detail_page.dart @@ -140,6 +140,7 @@ class _DetailPageState extends State { ), extendBodyBehindAppBar: true, resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, body: Center( child: Stack( children: [ @@ -165,6 +166,7 @@ class _DetailPageState extends State { Widget _buildPageView(BuildContext context) { return PageView.builder( + clipBehavior: Clip.none, itemBuilder: (context, index) { final file = _files![index]; _preloadFiles(index); diff --git a/mobile/lib/ui/viewer/file/file_caption_widget.dart b/mobile/lib/ui/viewer/file/file_caption_widget.dart index 446c9ce55..bec10c7b2 100644 --- a/mobile/lib/ui/viewer/file/file_caption_widget.dart +++ b/mobile/lib/ui/viewer/file/file_caption_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/components/keyboard/keybiard_oveylay.dart'; +import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart'; import 'package:photos/ui/components/keyboard/keyboard_top_button.dart'; import 'package:photos/utils/magic_util.dart'; diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index be265730e..ff6bceb73 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'dart:io' as io; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import "package:flutter_image_compress/flutter_image_compress.dart"; import 'package:logging/logging.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photos/core/cache/thumbnail_in_memory_cache.dart'; @@ -50,6 +51,7 @@ class _ZoomableImageState extends State { bool _loadedLargeThumbnail = false; bool _loadingFinalImage = false; bool _loadedFinalImage = false; + bool _convertToSupportedFormat = false; ValueChanged? _scaleStateChangedCallback; bool _isZooming = false; PhotoViewController _photoViewController = PhotoViewController(); @@ -57,6 +59,7 @@ class _ZoomableImageState extends State { @override void initState() { + super.initState(); _photo = widget.photo; _logger = Logger("ZoomableImage"); _logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}'); @@ -68,7 +71,6 @@ class _ZoomableImageState extends State { debugPrint("isZooming = $_isZooming, currentState $value"); // _logger.info('is reakky zooming $_isZooming with state $value'); }; - super.initState(); } @override @@ -133,7 +135,9 @@ class _ZoomableImageState extends State { height: screenRelativeImageHeight, child: Hero( tag: widget.tagPrefix! + _photo.tag, - child: const EnteLoadingWidget(), + child: const EnteLoadingWidget( + color: Colors.white, + ), ), ), ); @@ -141,7 +145,9 @@ class _ZoomableImageState extends State { ), ); } else { - content = const EnteLoadingWidget(); + content = const EnteLoadingWidget( + color: Colors.white, + ); } final GestureDragUpdateCallback? verticalDragCallback = _isZooming @@ -194,11 +200,8 @@ class _ZoomableImageState extends State { _loadingFinalImage = true; getFileFromServer(_photo).then((file) { if (file != null) { - _onFinalImageLoaded( - Image.file( - file, - gaplessPlayback: true, - ).image, + _onFileLoaded( + file, ); } else { _loadingFinalImage = false; @@ -235,11 +238,13 @@ class _ZoomableImageState extends State { _loadingFinalImage = true; getFile( _photo, - isOrigin: io.Platform.isIOS && + isOrigin: Platform.isIOS && _isGIF(), // since on iOS GIFs playback only when origin-files are loaded ).then((file) { if (file != null && file.existsSync()) { - _onFinalImageLoaded(Image.file(file).image); + _onFileLoaded( + file, + ); } else { _logger.info("File was deleted " + _photo.toString()); if (_photo.uploadedFileID != null) { @@ -277,43 +282,45 @@ class _ZoomableImageState extends State { } } - void _onFinalImageLoaded(ImageProvider imageProvider) async { - // // final result = await FaceMlService.instance.analyzeImage( - // // _photo, - // // preferUsingThumbnailForEverything: false, - // // disposeImageIsolateAfterUse: false, - // // ); - // // _logger.info("FaceMlService result: $result"); - // // _logger.info("Number of faces detected: ${result.faces.length}"); - // // _logger.info("Box: ${result.faces[0].detection.box}"); - // // _logger.info("Landmarks: ${result.faces[0].detection.allKeypoints}"); - // // final embedding = result.faces[0].embedding; - // // Calculate the magnitude of the embedding vector - // double sum = 0; - // for (final double value in embedding) { - // sum += value * value; - // } - // final magnitude = math.sqrt(sum); - // log("Magnitude: $magnitude"); - // log("Embedding: $embedding"); + void _onFileLoaded(File file) { + final imageProvider = Image.file( + file, + gaplessPlayback: true, + ).image; + if (mounted) { - // ignore: unawaited_futures - precacheImage(imageProvider, context).then((value) async { - if (mounted) { - await _updatePhotoViewController( - previewImageProvider: _imageProvider, - finalImageProvider: imageProvider, - ); - setState(() { - _imageProvider = imageProvider; - _loadedFinalImage = true; - _logger.info("Final image loaded"); - }); + precacheImage( + imageProvider, + context, + onError: (exception, _) async { + _logger + .info(exception.toString() + ". Filename: ${_photo.displayName}"); + if (exception.toString().contains( + "Codec failed to produce an image, possibly due to invalid image data", + )) { + unawaited(_loadInSupportedFormat(file)); + } + }, + ).then((value) { + if (mounted && !_loadedFinalImage && !_convertToSupportedFormat) { + _updateViewWithFinalImage(imageProvider); } }); } } + Future _updateViewWithFinalImage(ImageProvider imageProvider) async { + await _updatePhotoViewController( + previewImageProvider: _imageProvider, + finalImageProvider: imageProvider, + ); + setState(() { + _imageProvider = imageProvider; + _loadedFinalImage = true; + _logger.info("Final image loaded"); + }); + } + Future _updatePhotoViewController({ required ImageProvider? previewImageProvider, required ImageProvider finalImageProvider, @@ -367,4 +374,28 @@ class _ZoomableImageState extends State { } bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif"); + + Future _loadInSupportedFormat(File file) async { + _logger.info("Compressing ${_photo.displayName} to viewable format"); + _convertToSupportedFormat = true; + + final compressedFile = + await FlutterImageCompress.compressWithFile(file.path); + + if (compressedFile != null) { + final imageProvider = MemoryImage(compressedFile); + + unawaited( + precacheImage(imageProvider, context).then((value) { + if (mounted) { + _updateViewWithFinalImage(imageProvider); + } + }), + ); + } else { + _logger.severe( + "Failed to compress image ${_photo.displayName} to viewable format", + ); + } + } } diff --git a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart index 0f2510d74..78dd4a424 100644 --- a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart +++ b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart @@ -21,6 +21,7 @@ import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/photo_manager_util.dart"; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; Future showAddPhotosSheet( @@ -203,7 +204,7 @@ class AddPhotosPhotoWidget extends StatelessWidget { } } catch (e) { if (e is StateError) { - final PermissionState ps = await PhotoManager.requestPermissionExtend(); + final PermissionState ps = await requestPhotoMangerPermissions(); if (ps != PermissionState.authorized && ps != PermissionState.limited) { await showChoiceDialog( context, diff --git a/mobile/lib/ui/viewer/location/add_location_sheet.dart b/mobile/lib/ui/viewer/location/add_location_sheet.dart index cf6629431..9e1345dc0 100644 --- a/mobile/lib/ui/viewer/location/add_location_sheet.dart +++ b/mobile/lib/ui/viewer/location/add_location_sheet.dart @@ -12,7 +12,7 @@ import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/bottom_of_title_bar_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/divider_widget.dart"; -import "package:photos/ui/components/keyboard/keybiard_oveylay.dart"; +import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart'; import "package:photos/ui/components/keyboard/keyboard_top_button.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/text_input_widget.dart"; diff --git a/mobile/lib/ui/viewer/location/edit_location_sheet.dart b/mobile/lib/ui/viewer/location/edit_location_sheet.dart index b5c517a66..66df52e90 100644 --- a/mobile/lib/ui/viewer/location/edit_location_sheet.dart +++ b/mobile/lib/ui/viewer/location/edit_location_sheet.dart @@ -13,7 +13,7 @@ import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/bottom_of_title_bar_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/divider_widget.dart"; -import "package:photos/ui/components/keyboard/keybiard_oveylay.dart"; +import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart'; import "package:photos/ui/components/keyboard/keyboard_top_button.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/text_input_widget.dart"; diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 89a12f147..7c898f985 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -9,6 +9,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; +import "package:permission_handler/permission_handler.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/core/constants.dart"; import 'package:photos/core/errors.dart'; @@ -363,6 +364,15 @@ class FileUploader { } } + Future verifyMediaLocationAccess() async { + if (Platform.isAndroid) { + final bool hasPermission = await Permission.accessMediaLocation.isGranted; + if (!hasPermission) { + throw NoMediaLocationAccessError(); + } + } + } + Future forceUpload(EnteFile file, int collectionID) async { _hasInitiatedForceUpload = true; return _tryToUpload(file, collectionID, true); diff --git a/mobile/lib/utils/photo_manager_util.dart b/mobile/lib/utils/photo_manager_util.dart new file mode 100644 index 000000000..273d0b362 --- /dev/null +++ b/mobile/lib/utils/photo_manager_util.dart @@ -0,0 +1,12 @@ +import "package:photo_manager/photo_manager.dart"; + +Future requestPhotoMangerPermissions() { + return PhotoManager.requestPermissionExtend( + requestOption: const PermissionRequestOption( + androidPermission: AndroidPermission( + type: RequestType.common, + mediaLocation: true, + ), + ), + ); +} diff --git a/mobile/lib/utils/share_util.dart b/mobile/lib/utils/share_util.dart index 1f147ab8f..ff9f691bd 100644 --- a/mobile/lib/utils/share_util.dart +++ b/mobile/lib/utils/share_util.dart @@ -15,6 +15,7 @@ import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:share_plus/share_plus.dart'; +import "package:uuid/uuid.dart"; final _logger = Logger("ShareUtil"); // Set of possible image extensions @@ -116,14 +117,13 @@ Future> convertIncomingSharedMediaToFile( continue; } final enteFile = EnteFile(); + final sharedLocalId = const Uuid().v4(); // fileName: img_x.jpg enteFile.title = basename(media.path); var ioFile = File(media.path); try { ioFile = ioFile.renameSync( - Configuration.instance.getSharedMediaDirectory() + - "/" + - enteFile.title!, + Configuration.instance.getSharedMediaDirectory() + "/" + sharedLocalId, ); } catch (e) { if (e is FileSystemException) { @@ -135,7 +135,7 @@ Future> convertIncomingSharedMediaToFile( final newIoFile = ioFile.copySync( Configuration.instance.getSharedMediaDirectory() + "/" + - enteFile.title!, + sharedLocalId, ); if (media.path.contains("io.ente.photos")) { _logger.info("delete original file in path ${ioFile.path}"); @@ -146,7 +146,7 @@ Future> convertIncomingSharedMediaToFile( rethrow; } } - enteFile.localID = sharedMediaIdentifier + enteFile.title!; + enteFile.localID = sharedMediaIdentifier + sharedLocalId; enteFile.collectionID = collectionID; enteFile.fileType = media.type == SharedMediaType.image ? FileType.image : FileType.video; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 857e65ffc..c1519ddad 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -650,7 +650,7 @@ packages: source: hosted version: "0.6.0" flutter_driver: - dependency: "direct dev" + dependency: transitive description: flutter source: sdk version: "0.0.0" @@ -941,12 +941,11 @@ packages: home_widget: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: "49158ce4a517e87817dc84c6b96c00639281229a" - url: "https://github.com/prateekmedia/FlutterHomeWidget" - source: git - version: "0.4.1" + name: home_widget + sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3" + url: "https://pub.dev" + source: hosted + version: "0.5.0" html: dependency: transitive description: @@ -1555,6 +1554,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + url: "https://pub.dev" + source: hosted + version: "11.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 7382164b5..719440b3f 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.71+591 +version: 0.8.74+594 publish_to: none environment: @@ -92,10 +92,7 @@ dependencies: fluttertoast: ^8.0.6 freezed_annotation: ^2.2.0 google_nav_bar: ^5.0.5 - home_widget: - git: - url: https://github.com/prateekmedia/FlutterHomeWidget - ref: main + home_widget: ^0.5.0 html_unescape: ^2.0.0 http: ^1.1.0 image: ^4.0.17 @@ -135,6 +132,7 @@ dependencies: path: #dart path_provider: ^2.1.1 pedantic: ^1.9.2 + permission_handler: ^11.0.1 photo_manager: ^2.8.1 photo_view: ^0.14.0 pinput: ^1.2.2 @@ -196,8 +194,6 @@ flutter_intl: dev_dependencies: build_runner: ^2.4.7 - flutter_driver: - sdk: flutter flutter_lints: ^2.0.1 flutter_test: sdk: flutter diff --git a/mobile/test_driver/perf_driver.dart b/mobile/test_driver/perf_driver.dart index b993c135e..ecc0bd6b0 100644 --- a/mobile/test_driver/perf_driver.dart +++ b/mobile/test_driver/perf_driver.dart @@ -8,13 +8,13 @@ Future main() { responseDataCallback: (data) async { if (data != null) { final timeline = driver.Timeline.fromJson( - data['scrolling_summary'] as Map, + data['home_gallery_scrolling_summary'] as Map, ); final summary = driver.TimelineSummary.summarize(timeline); await summary.writeTimelineToFile( - 'scrolling_summary', + 'home_gallery_scrolling_summary', pretty: true, includeSummary: true, //Specify destination directory for the timeline files. diff --git a/server/README.md b/server/README.md index 06fdcb518..10e1d9880 100644 --- a/server/README.md +++ b/server/README.md @@ -38,7 +38,13 @@ And ping again This time you'll see the updated message. For more details about how to get museum up and running, see -[RUNNING.md](RUNNING.md). +[RUNNING](RUNNING.md). + +> [!TIP] +> +> Also, there is a way to use our pre-built Docker images to directly start a +> cluster without needing to clone this repository - see +> [docs/docker](docs/docker.md). ## Architecture @@ -84,20 +90,22 @@ And it is built with containerization in mind - both during development and deployment. Just use the provided Dockerfile, configure to taste and you're off to the races. -> [!CAUTION] -> -> We don't publish any official docker images (yet). For self-hosters, the -> recommendation is to build your own image using the provided `Dockerfile`. +Overall, there are [three approaches](RUNNING.md) you can take: + +* Run using Docker using a pre-built Docker image +* Run using Docker but build an image from source +* Run without Docker Everything that you might needed to run museum is all in here, since this is the -setup we ourselves use in production. +code we ourselves use in production. > [!TIP] > > On our production servers, we wrap museum in a [systemd -> service](scripts/museum.service). Our production machines are vanilla Ubuntu -> images, with Docker and Promtail installed. We then plonk in this systemd -> service, and use `systemctl start|stop|status museum` to herd it around. +> service](scripts/deploy/museum.service). Our production machines are vanilla +> Ubuntu images, with Docker and Promtail installed. We then plonk in this +> systemd service, and use `systemctl start|stop|status museum` to herd it +> around. Some people new to Docker/Go/Postgres might have general questions though. Unfortunately, because of limited engineering bandwidth **we will currently not diff --git a/server/RUNNING.md b/server/RUNNING.md index 22045fe2b..9410650e0 100644 --- a/server/RUNNING.md +++ b/server/RUNNING.md @@ -8,13 +8,14 @@ environment that doesn't clutter your machine. You can also run museum directly on your machine if you wish - it is a single static go binary. -This document describes both these approaches, and also outlines configuration. +This document describes these approaches, and also outlines configuration. -- [Running using Docker](#docker) -- [Running without Docker](#without-docker) +- [Run using Docker using a pre-built Docker image](docs/docker.md) +- [Run using Docker but build an image from source](#build-and-run-using-docker) +- [Running without Docker](#run-without-docker) - [Configuration](#configuration) -## Docker +## Build and run using Docker Start the cluster @@ -43,6 +44,12 @@ Or interact with the MinIO S3 API Or open the MinIO dashboard at (user: test/password: testtest). +> [!NOTE] +> +> While we've provided a MinIO based Docker compose file to make it easy for +> people to get started, if you're running it in production we recommend using +> an external S3. + > [!NOTE] > > If something seems amiss, ensure that Docker has read access to the parent @@ -70,7 +77,7 @@ Each time museum gets rebuilt from source, a new image gets created but the old one is retained as a dangling image. You can use `docker image prune --force`, or `docker system prune` if that's fine with you, to remove these. -## Without Docker +## Running without Docker The museum binary can be run by using `go run cmd/museum/main.go`. But first, you'll need to prepare your machine for development. Here we give the steps, @@ -132,7 +139,7 @@ pg_ctl -D /usr/local/var/postgres -l logfile start createuser -s postgres ``` -## Start museum +### Start museum ```sh export ENTE_DB_USER=postgres @@ -148,7 +155,7 @@ ENTE_DB_USER=ente_user air ``` -## Testing +### Testing Set up a local database for testing. This is not required for running the server. Create a test database with the following name and credentials: diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 14ceb3e3b..4cbc00612 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -37,7 +37,6 @@ import ( embeddingCtrl "github.com/ente-io/museum/pkg/controller/embedding" "github.com/ente-io/museum/pkg/controller/family" kexCtrl "github.com/ente-io/museum/pkg/controller/kex" - "github.com/ente-io/museum/pkg/controller/locationtag" "github.com/ente-io/museum/pkg/controller/lock" remoteStoreCtrl "github.com/ente-io/museum/pkg/controller/remotestore" "github.com/ente-io/museum/pkg/controller/storagebonus" @@ -50,7 +49,6 @@ import ( "github.com/ente-io/museum/pkg/repo/datacleanup" "github.com/ente-io/museum/pkg/repo/embedding" "github.com/ente-io/museum/pkg/repo/kex" - locationtagRepo "github.com/ente-io/museum/pkg/repo/locationtag" "github.com/ente-io/museum/pkg/repo/passkey" "github.com/ente-io/museum/pkg/repo/remotestore" storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus" @@ -150,7 +148,6 @@ func main() { twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes} billingRepo := &repo.BillingRepository{DB: db} userEntityRepo := &userEntityRepo.Repository{DB: db} - locationTagRepository := &locationtagRepo.Repository{DB: db} authRepo := &authenticatorRepo.Repository{DB: db} remoteStoreRepository := &remotestore.Repository{DB: db} dataCleanupRepository := &datacleanup.Repository{DB: db} @@ -178,7 +175,8 @@ func main() { authCache := cache.New(1*time.Minute, 15*time.Minute) accessTokenCache := cache.New(1*time.Minute, 15*time.Minute) discordController := discord.NewDiscordController(userRepo, hostName, environment) - rateLimiter := middleware.NewRateLimitMiddleware(discordController) + rateLimiter := middleware.NewRateLimitMiddleware(discordController, 1000, 1*time.Second) + defer rateLimiter.Stop() emailNotificationCtrl := &email.EmailNotificationController{ UserRepo: userRepo, @@ -360,22 +358,22 @@ func main() { server.Use(requestid.New(), middleware.Logger(urlSanitizer), cors(), gzip.Gzip(gzip.DefaultCompression), middleware.PanicRecover()) publicAPI := server.Group("/") - publicAPI.Use(rateLimiter.APIRateLimitMiddleware(urlSanitizer)) + publicAPI.Use(rateLimiter.GlobalRateLimiter(), rateLimiter.APIRateLimitMiddleware(urlSanitizer)) privateAPI := server.Group("/") - privateAPI.Use(authMiddleware.TokenAuthMiddleware(nil), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) + privateAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(nil), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) adminAPI := server.Group("/admin") - adminAPI.Use(authMiddleware.TokenAuthMiddleware(nil), authMiddleware.AdminAuthMiddleware()) + adminAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(nil), authMiddleware.AdminAuthMiddleware()) paymentJwtAuthAPI := server.Group("/") - paymentJwtAuthAPI.Use(authMiddleware.TokenAuthMiddleware(jwt.PAYMENT.Ptr())) + paymentJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.PAYMENT.Ptr())) familiesJwtAuthAPI := server.Group("/") //The middleware order matters. First, the userID must be set in the context, so that we can apply limit for user. - familiesJwtAuthAPI.Use(authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) + familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) publicCollectionAPI := server.Group("/public-collection") - publicCollectionAPI.Use(accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer)) + publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer)) healthCheckHandler := &api.HealthCheckHandler{ DB: db, @@ -472,7 +470,7 @@ func main() { privateAPI.DELETE("/users/delete", userHandler.DeleteUser) accountsJwtAuthAPI := server.Group("/") - accountsJwtAuthAPI.Use(authMiddleware.TokenAuthMiddleware(jwt.ACCOUNTS.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) + accountsJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.ACCOUNTS.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) passkeysHandler := &api.PasskeyHandler{ Controller: passkeyCtrl, } @@ -531,7 +529,7 @@ func main() { castCtrl := cast.NewController(&castDb, accessCtrl) castMiddleware := middleware.CastMiddleware{CastCtrl: castCtrl, Cache: authCache} - castAPI.Use(castMiddleware.CastAuthMiddleware()) + castAPI.Use(rateLimiter.GlobalRateLimiter(), castMiddleware.CastAuthMiddleware()) castHandler := &api.CastHandler{ CollectionCtrl: collectionController, @@ -640,13 +638,6 @@ func main() { privateAPI.DELETE("/user-entity/entity", userEntityHandler.DeleteEntity) privateAPI.GET("/user-entity/entity/diff", userEntityHandler.GetDiff) - locationTagController := &locationtag.Controller{Repo: locationTagRepository} - locationTagHandler := &api.LocationTagHandler{Controller: locationTagController} - privateAPI.POST("/locationtag/create", locationTagHandler.Create) - privateAPI.POST("/locationtag/update", locationTagHandler.Update) - privateAPI.DELETE("/locationtag/delete", locationTagHandler.Delete) - privateAPI.GET("/locationtag/diff", locationTagHandler.GetDiff) - authenticatorController := &authenticatorCtrl.Controller{Repo: authRepo} authenticatorHandler := &api.AuthenticatorHandler{Controller: authenticatorController} @@ -680,6 +671,7 @@ func main() { privateAPI.PUT("/embeddings", embeddingHandler.InsertOrUpdate) privateAPI.GET("/embeddings/diff", embeddingHandler.GetDiff) + privateAPI.POST("/embeddings/files", embeddingHandler.GetFilesEmbedding) privateAPI.DELETE("/embeddings", embeddingHandler.DeleteAll) offerHandler := &api.OfferHandler{Controller: offerController} @@ -711,9 +703,8 @@ func main() { } func runServer(environment string, server *gin.Engine) { - if environment == "local" { - server.Run(":8080") - } else { + useTLS := viper.GetBool("http.use-tls") + if useTLS { certPath, err := config.CredentialFilePath("tls.cert") if err != nil { log.Fatal(err) @@ -725,6 +716,8 @@ func runServer(environment string, server *gin.Engine) { } log.Fatal(server.RunTLS(":443", certPath, keyPath)) + } else { + server.Run(":8080") } } diff --git a/server/compose.yaml b/server/compose.yaml index 6972fc364..a7d5a2c39 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -17,6 +17,7 @@ services: - custom-logs:/var/logs - ./museum.yaml:/museum.yaml:ro - ./scripts/compose/credentials.yaml:/credentials.yaml:ro + - ./data:/data:ro networks: - internal diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index 47d6f0d37..97dd353e1 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -65,6 +65,12 @@ # It must be specified if running in a non-local environment. log-file: "" +# HTTP connection parameters +http: + # If true, bind to 443 and use TLS. + # By default, this is false, and museum will bind to 8080 without TLS. + # use-tls: true + # Database connection parameters db: host: localhost @@ -113,10 +119,7 @@ s3: # # 1. Disable SSL. # - # 2. Use "path" style S3 URLs where the bucket is part of the URL path, e.g. - # http://localhost:3200/b2-eu-cen. By default the bucket name is part of - # the (sub)domain, e.g. http://b2-eu-cen.localhost:3200/ and cannot be - # resolved when running locally. + # 2. Use "path" style S3 URLs (see `use_path_style_urls` below). # # 3. Directly download the file during replication instead of going via the # Cloudflare worker. @@ -125,6 +128,17 @@ s3: # not support them, specifically it doesn't support GLACIER). # #are_local_buckets: true + # Uncomment this to use "path" style S3 URLs. + # + # By default the bucket name is part of the (sub)domain, e.g. + # http://b2-eu-cen.localhost:3200/. If this is true, then we use "path" + # style S3 URLs where the bucket is part of the URL path, e.g. + # http://localhost:3200/b2-eu-cen. + # + # This is useful in scenarios when sub-domain based addressing cannot be + # resolved, e.g. when running a local instance, or when using MinIO as a + # production store. + #use_path_style_urls: true # Key used for encrypting customer emails before storing them in DB # @@ -143,8 +157,24 @@ key: jwt: secret: i2DecQmfGreG6q1vBj5tCokhlN41gcfS2cjOs9Po-u8= +# SMTP configuration (optional) +# +# Configure credentials here for sending mails from museum (e.g. OTP emails). +# +# The smtp credentials will be used if the host is specified. Otherwise it will +# try to use the transmail credentials. Ideally, one of smtp or transmail should +# be configured for a production instance. +smtp: + host: + port: + username: + password: + # Zoho Zeptomail config (optional) -# Use case: Sending emails +# +# This is an alternative to the `smtp` configuration for sending emails. If this +# is set (and SMTP credentials are not set), then museum will use the transmail +# SDK for sending emails using Zoho Zeptomail. transmail: # Transmail token # Mail agent: dev @@ -174,7 +204,7 @@ stripe: webauthn: rpid: "example.com" rporigins: - - "https://example.com:3005" + - "https://example.com:3005" # Roadmap SSO (optional) # @@ -220,13 +250,17 @@ internal: # If provided, this external healthcheck url is periodically pinged. health-check-url: # Hardcoded verification codes, useful for logging in when developing. - hardcoded-ott: - emails: - - "example@example.org,123456" - # When running in a local environment, hardcode the verification code to - # 123456 for email addresses ending with @example.org - local-domain-suffix: "@example.org" - local-domain-value: 123456 + # + # Uncomment this and set these to your email ID or domain so that you don't + # need to peek into the server logs for obtaining the OTP when trying to log + # into an instance you're developing on. + # hardcoded-ott: + # emails: + # - "example@example.org,123456" + # # When running in a local environment, hardcode the verification code to + # # 123456 for email addresses ending with @example.org + # local-domain-suffix: "@example.org" + # local-domain-value: 123456 # List of user IDs that can use the admin API endpoints. admins: [] @@ -261,4 +295,4 @@ jobs: # By default, this job is disabled. enabled: false # If provided, only objects that begin with this prefix are pruned. - prefix: "" + prefix: "" \ No newline at end of file diff --git a/server/configurations/production.yaml b/server/configurations/production.yaml index 6c7c20b81..937d0c907 100644 --- a/server/configurations/production.yaml +++ b/server/configurations/production.yaml @@ -1,5 +1,8 @@ log-file: /var/logs/museum.log +http: + use-tls: true + stripe: path: success: ?status=success&session_id={CHECKOUT_SESSION_ID} diff --git a/server/docs/docker.md b/server/docs/docker.md new file mode 100644 index 000000000..d8f3db913 --- /dev/null +++ b/server/docs/docker.md @@ -0,0 +1,83 @@ +# Running using published Docker images + +Here we describe a way to run an Ente instance using a starter Docker compose +file and using the pre-built Docker images that we publish. This method does not +require you to clone the repository or build any images. + +1. Create a directory where you'll run Ente + + ```sh + mkdir ente && cd ente + ``` + +2. Copy the starter compose.yaml and two of its support files from the + repository onto your directory. You can do it by hand, or use (e.g.) curl + + ```sh + # compose.yaml + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/compose.yaml + + mkdir -p scripts/compose + cd scripts/compose + + # scripts/compose/credentials.yaml + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/scripts/compose/credentials.yaml + + # scripts/compose/minio-provision.sh + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/scripts/compose/minio-provision.sh + + cd ../.. + ``` + +3. Modify `compose.yaml`. Instead of building from source, we want directly use + the published Docker image from `ghcr.io/ente-io/server` + + ```diff + --- a/server/compose.yaml + +++ b/server/compose.yaml + @@ -1,9 +1,6 @@ + services: + museum: + - build: + - context: . + - args: + - GIT_COMMIT: development-cluster + + image: ghcr.io/ente-io/server + ``` + +4. Create an (empty) configuration file. Yyou can later put your custom + configuration in this if needed. + + ```sh + touch museum.yaml + ``` + +5. That is all. You can now start everything. + + ```sh + docker compose up + ``` + +This will start a cluster containing: + +* Ente's own server +* PostgresQL (DB) +* MinIO (the S3 layer) + +For each of these, it'll use the latest published Docker image. + +You can do a quick smoke test by pinging the API: + +```sh +curl localhost:8080/ping +``` + +## Only the server + +Alternatively, if you have setup the database and object storage externally and +only want to run Ente's server, you can skip the steps above and directly pull +and run the image from **`ghcr.io/ente-io/server`**. + +```sh +docker pull ghcr.io/ente-io/server +``` diff --git a/server/docs/publish.md b/server/docs/publish.md new file mode 100644 index 000000000..de4849d90 --- /dev/null +++ b/server/docs/publish.md @@ -0,0 +1,41 @@ +# Publishing images + +There are two different images we publish - internal and external. + +## Internal + +The internal images can be built and run by triggering the "Server (release)" +workflow. You can trigger it either from GitHub's UI on the Actions tab, or use +the following command: + + gh workflow run server-release.yml + +This will take the latest main, package it into a Docker image, and publish it +to our Scaleway registry. From there, we can update our production instances to +use this new image (see [deploy/README](../scripts/deploy/README.md)). + +## External + +Periodically, we can republish a new image from an existing known-to-be-good +commit to the GitHub Container Registry (GHCR) so that it can be used by folks +without needing to clone our repository just for building an image. For more +details about the use case, see [docker.md](docker.md). + +To publish such an external image, firstly find the commit of the currently +running production instance. + + curl -s https://api.ente.io/ping | jq -r '.id' + +> We can publish from any arbitrary commit really, but by using the commit +> that's already seen production for a few days, we avoid externally publishing +> images with issues. + +Then, trigger the "Publish (server)" workflow, providing it the commit. You can +trigger it either from GitHub's UI or using the `gh cli`. With the CLI, we can +combine both these steps too. + + gh workflow run server-publish.yml -F commit=`curl -s https://api.ente.io/ping | jq -r '.id'` + +Once the workflow completes, the resultant image will be available at +`ghcr.io/ente-io/server`. The image will be tagged by the commit SHA. The latest +image will also be tagged, well, "latest". diff --git a/server/ente/billing.go b/server/ente/billing.go index 5a0ff08a8..20c37bdb5 100644 --- a/server/ente/billing.go +++ b/server/ente/billing.go @@ -42,7 +42,7 @@ const ( OnHoldTemplate = "on_hold.html" // AccountOnHoldEmailSubject is the subject of account on hold email - AccountOnHoldEmailSubject = "ente account on hold" + AccountOnHoldEmailSubject = "Ente account on hold" // Template for the email we send out when the user's subscription ends, // either because the user cancelled their subscription, or because it diff --git a/server/ente/embedding.go b/server/ente/embedding.go index b59332ec6..2990a779a 100644 --- a/server/ente/embedding.go +++ b/server/ente/embedding.go @@ -6,6 +6,7 @@ type Embedding struct { EncryptedEmbedding string `json:"encryptedEmbedding"` DecryptionHeader string `json:"decryptionHeader"` UpdatedAt int64 `json:"updatedAt"` + Version *int `json:"version,omitempty"` } type InsertOrUpdateEmbeddingRequest struct { @@ -13,6 +14,7 @@ type InsertOrUpdateEmbeddingRequest struct { Model string `json:"model" binding:"required"` EncryptedEmbedding string `json:"encryptedEmbedding" binding:"required"` DecryptionHeader string `json:"decryptionHeader" binding:"required"` + Version *int `json:"version,omitempty"` } type GetEmbeddingDiffRequest struct { @@ -22,11 +24,25 @@ type GetEmbeddingDiffRequest struct { Limit int16 `form:"limit" binding:"required"` } +type GetFilesEmbeddingRequest struct { + Model Model `form:"model" binding:"required"` + FileIDs []int64 `form:"fileIDs" binding:"required"` +} + +type GetFilesEmbeddingResponse struct { + Embeddings []Embedding `json:"embeddings"` + NoDataFileIDs []int64 `json:"noDataFileIDs"` + ErrFileIDs []int64 `json:"errFileIDs"` +} + type Model string const ( OnnxClip Model = "onnx-clip" GgmlClip Model = "ggml-clip" + + // FileMlClipFace is a model for face embeddings, it is used in request validation. + FileMlClipFace Model = "file-ml-clip-face" ) type EmbeddingObject struct { diff --git a/server/ente/locationtag.go b/server/ente/locationtag.go deleted file mode 100644 index 61c191006..000000000 --- a/server/ente/locationtag.go +++ /dev/null @@ -1,59 +0,0 @@ -package ente - -import ( - "database/sql/driver" - "encoding/json" - "github.com/ente-io/stacktrace" - "github.com/google/uuid" -) - -// LocationTag represents a location tag in the system. The location information -// is stored in an encrypted as Attributes -type LocationTag struct { - ID uuid.UUID `json:"id"` - OwnerID int64 `json:"ownerId,omitempty"` - EncryptedKey string `json:"encryptedKey" binding:"required"` - KeyDecryptionNonce string `json:"keyDecryptionNonce" binding:"required"` - Attributes LocationTagAttribute `json:"attributes" binding:"required"` - IsDeleted bool `json:"isDeleted"` - Provider string `json:"provider,omitempty"` - CreatedAt int64 `json:"createdAt,omitempty"` // utc epoch microseconds - UpdatedAt int64 `json:"updatedAt,omitempty"` // utc epoch microseconds -} - -// LocationTagAttribute holds encrypted data about user's location tag. -type LocationTagAttribute struct { - Version int `json:"version,omitempty" binding:"required"` - EncryptedData string `json:"encryptedData,omitempty" binding:"required"` - DecryptionNonce string `json:"decryptionNonce,omitempty" binding:"required"` -} - -// Value implements the driver.Valuer interface. This method -// simply returns the JSON-encoded representation of the struct. -func (la LocationTagAttribute) Value() (driver.Value, error) { - return json.Marshal(la) -} - -// Scan implements the sql.Scanner interface. This method -// simply decodes a JSON-encoded value into the struct fields. -func (la *LocationTagAttribute) Scan(value interface{}) error { - b, ok := value.([]byte) - if !ok { - return stacktrace.NewError("type assertion to []byte failed") - } - return json.Unmarshal(b, &la) -} - -// DeleteLocationTagRequest is request structure for deleting a location tag -type DeleteLocationTagRequest struct { - ID uuid.UUID `json:"id" binding:"required"` - OwnerID int64 // should be populated from req headers -} - -// GetLocationTagDiffRequest is request struct for fetching locationTag changes -type GetLocationTagDiffRequest struct { - // SinceTime *int64. Pointer allows us to pass 0 value otherwise binding fails for zero Value. - SinceTime *int64 `form:"sinceTime" binding:"required"` - Limit int16 `form:"limit" binding:"required"` - OwnerID int64 // should be populated from req headers -} diff --git a/server/ente/userentity/entity.go b/server/ente/userentity/entity.go index 729b69e45..71baa3ae9 100644 --- a/server/ente/userentity/entity.go +++ b/server/ente/userentity/entity.go @@ -8,6 +8,7 @@ type EntityType string const ( Location EntityType = "location" + Person EntityType = "person" ) type EntityKey struct { diff --git a/server/mail-templates/mobile_app_first_upload.html b/server/mail-templates/mobile_app_first_upload.html index 53d7cfa86..1ee954672 100644 --- a/server/mail-templates/mobile_app_first_upload.html +++ b/server/mail-templates/mobile_app_first_upload.html @@ -302,7 +302,7 @@ About FAQ + href="https://help.ente.io/">Help Twitter @@ -234,7 +234,7 @@ from {{.PaymentProvider}} within the next - 30 days, our systems + 31 days, our systems may remove your account and all associated data with diff --git a/server/mail-templates/subscription_ended.html b/server/mail-templates/subscription_ended.html index 4a158d767..a60fe0e2f 100644 --- a/server/mail-templates/subscription_ended.html +++ b/server/mail-templates/subscription_ended.html @@ -214,7 +214,7 @@
If you still have data stored in ente, we encourage you to follow the steps outlined here to export your data: ente.io/faq/migration/out-of-ente. + style="font-family: helvetica, sans-serif">If you still have data stored in ente, we encourage you to follow the steps outlined here to export your data: help.ente.io/photos/migration/export.
diff --git a/server/mail-templates/successful_referral.html b/server/mail-templates/successful_referral.html index 60812e0fd..fd7f3ea23 100644 --- a/server/mail-templates/successful_referral.html +++ b/server/mail-templates/successful_referral.html @@ -253,7 +253,7 @@ About FAQ + href="https://help.ente.io/">Help Twitter About FAQ + href="https://help.ente.io/">Help Twitter 200 { + return ente.NewBadRequestWithMessage("fileIDs should be less than or equal to 200") + } + if err := c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ + ActorUserId: userID, + FileIDs: req.FileIDs, + }); err != nil { + return stacktrace.Propagate(err, "User does not own some file(s)") + } + return nil +} diff --git a/server/pkg/controller/locationtag/controller.go b/server/pkg/controller/locationtag/controller.go deleted file mode 100644 index 9b9618117..000000000 --- a/server/pkg/controller/locationtag/controller.go +++ /dev/null @@ -1,31 +0,0 @@ -package locationtag - -import ( - "github.com/ente-io/museum/ente" - "github.com/ente-io/museum/pkg/repo/locationtag" - "github.com/gin-gonic/gin" -) - -// Controller is interface for exposing business logic related to location tags -type Controller struct { - Repo *locationtag.Repository -} - -// Create a new location tag in the system -func (c *Controller) Create(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) { - return c.Repo.Create(ctx, req) -} -func (c *Controller) Update(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) { - // todo: verify ownership before updating - panic("implement me") -} - -// Delete the location tag for the given id and ownerId -func (c *Controller) Delete(ctx *gin.Context, req ente.DeleteLocationTagRequest) (bool, error) { - return c.Repo.Delete(ctx, req.ID.String(), req.OwnerID) -} - -// GetDiff fetches the locationTags which have changed after the specified time -func (c *Controller) GetDiff(ctx *gin.Context, req ente.GetLocationTagDiffRequest) ([]ente.LocationTag, error) { - return c.Repo.GetDiff(ctx, req.OwnerID, *req.SinceTime, req.Limit) -} diff --git a/server/pkg/controller/stripe.go b/server/pkg/controller/stripe.go index dc9f57eaf..9ce1b7e33 100644 --- a/server/pkg/controller/stripe.go +++ b/server/pkg/controller/stripe.go @@ -1,14 +1,15 @@ package controller import ( - "context" "database/sql" "encoding/json" "errors" "fmt" - "github.com/ente-io/museum/pkg/controller/commonbilling" "net/http" "strconv" + "time" + + "github.com/ente-io/museum/pkg/controller/commonbilling" "github.com/ente-io/museum/pkg/controller/discord" "github.com/ente-io/museum/pkg/controller/offer" @@ -24,7 +25,6 @@ import ( "github.com/spf13/viper" "github.com/stripe/stripe-go/v72" "github.com/stripe/stripe-go/v72/client" - "github.com/stripe/stripe-go/v72/invoice" "github.com/stripe/stripe-go/v72/webhook" "golang.org/x/text/currency" ) @@ -43,12 +43,7 @@ type StripeController struct { CommonBillCtrl *commonbilling.Controller } -// A flag we set on Stripe subscriptions to indicate that we should skip on -// sending out the email when the subscription has been cancelled. -// -// This is needed e.g. if this cancellation was as part of a user initiated -// account deletion. -const SkipMailKey = "skip_mail" +const BufferPeriodOnPaymentFailureInDays = 7 // Return a new instance of StripeController func NewStripeController(plans ente.BillingPlansPerAccount, stripeClients ente.StripeClientPerAccount, billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, storageBonusRepo *storagebonus.Repository, discordController *discord.DiscordController, emailNotificationController *emailCtrl.EmailNotificationController, offerController *offer.OfferController, commonBillCtrl *commonbilling.Controller) *StripeController { @@ -85,12 +80,15 @@ func (c *StripeController) GetCheckoutSession(productID string, userID int64, re return "", stacktrace.Propagate(ente.ErrBadRequest, "") } } - if subscription.PaymentProvider == ente.Stripe && !subscription.Attributes.IsCancelled { - // user had bought a stripe subscription earlier, - err := c.cancelExistingStripeSubscription(subscription, userID) + if hasStripeSubscription { + client := c.StripeClients[subscription.Attributes.StripeAccountCountry] + stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, nil) if err != nil { return "", stacktrace.Propagate(err, "") } + if stripeSubscription.Status != stripe.SubscriptionStatusCanceled { + return "", stacktrace.Propagate(ente.ErrBadRequest, "") + } } stripeSuccessURL := redirectRootURL + viper.GetString("stripe.path.success") stripeCancelURL := redirectRootURL + viper.GetString("stripe.path.cancel") @@ -160,7 +158,7 @@ func (c *StripeController) HandleUSNotification(payload []byte, header string) e if err != nil { return stacktrace.Propagate(err, "") } - return c.handleWebhookEvent(event) + return c.handleWebhookEvent(event, ente.StripeUS) } func (c *StripeController) HandleINNotification(payload []byte, header string) error { @@ -168,10 +166,10 @@ func (c *StripeController) HandleINNotification(payload []byte, header string) e if err != nil { return stacktrace.Propagate(err, "") } - return c.handleWebhookEvent(event) + return c.handleWebhookEvent(event, ente.StripeIN) } -func (c *StripeController) handleWebhookEvent(event stripe.Event) error { +func (c *StripeController) handleWebhookEvent(event stripe.Event, country ente.StripeAccountCountry) error { // The event body would already have been logged by the upper layers by the // time we get here, so we can only handle the events that we care about. In // case we receive an unexpected event, we do log an error though. @@ -180,7 +178,7 @@ func (c *StripeController) handleWebhookEvent(event stripe.Event) error { log.Error("Received an unexpected webhook from stripe:", event.Type) return nil } - eventLog, err := handler(event) + eventLog, err := handler(event, country) if err != nil { return stacktrace.Propagate(err, "") } @@ -196,16 +194,16 @@ func (c *StripeController) handleWebhookEvent(event stripe.Event) error { return stacktrace.Propagate(err, "") } -func (c *StripeController) findHandlerForEvent(event stripe.Event) func(event stripe.Event) (ente.StripeEventLog, error) { +func (c *StripeController) findHandlerForEvent(event stripe.Event) func(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) { switch event.Type { case "checkout.session.completed": return c.handleCheckoutSessionCompleted - case "customer.subscription.deleted": - return c.handleCustomerSubscriptionDeleted case "customer.subscription.updated": return c.handleCustomerSubscriptionUpdated case "invoice.paid": return c.handleInvoicePaid + case "payment_intent.payment_failed": + return c.handlePaymentIntentFailed default: return nil } @@ -213,7 +211,7 @@ func (c *StripeController) findHandlerForEvent(event stripe.Event) func(event st // Payment is successful and the subscription is created. // You should provision the subscription. -func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event) (ente.StripeEventLog, error) { +func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) { var session stripe.CheckoutSession json.Unmarshal(event.Data.Raw, &session) if session.ClientReferenceID != "" { // via payments.ente.io, where we inserted the userID @@ -268,61 +266,12 @@ func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event) (e return ente.StripeEventLog{}, nil } -// Occurs whenever a customer's subscription ends. -func (c *StripeController) handleCustomerSubscriptionDeleted(event stripe.Event) (ente.StripeEventLog, error) { - var stripeSubscription stripe.Subscription - json.Unmarshal(event.Data.Raw, &stripeSubscription) - currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscription.ID, ente.Stripe) - if err != nil { - // Ignore webhooks received before user has been created - // - // This would happen when we get webhook events out of order, e.g. we - // get a "customer.subscription.updated" before - // "checkout.session.completed", and the customer has not yet been - // created in our database. - if errors.Is(err, sql.ErrNoRows) { - log.Warn("Webhook is reporting an event for un-verified subscription stripeSubscriptionID:", stripeSubscription.ID) - return ente.StripeEventLog{}, nil - } - return ente.StripeEventLog{}, stacktrace.Propagate(err, "") - } - userID := currentSubscription.UserID - user, err := c.UserRepo.Get(userID) - if err != nil { - if errors.Is(err, ente.ErrUserDeleted) { - // no-op user has already been deleted - return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil - } - return ente.StripeEventLog{}, stacktrace.Propagate(err, "") - } - - skipMail := stripeSubscription.Metadata[SkipMailKey] - // Send a cancellation notification email for folks who are either on - // individual plan or admin of a family plan. - if skipMail != "true" && - (user.FamilyAdminID == nil || *user.FamilyAdminID == userID) { - storage, surpErr := c.StorageBonusRepo.GetPaidAddonSurplusStorage(context.Background(), userID) - if surpErr != nil { - return ente.StripeEventLog{}, stacktrace.Propagate(surpErr, "") - } - if storage == nil || *storage <= 0 { - err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io", - ente.SubscriptionEndedEmailSubject, ente.SubscriptionEndedEmailTemplate, - map[string]interface{}{}, nil) - if err != nil { - return ente.StripeEventLog{}, stacktrace.Propagate(err, "") - } - } else { - log.WithField("storage", storage).Info("User has surplus storage, not sending email") - } - } - // TODO: Add cron to delete files of users with expired subscriptions - return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil -} - -// Occurs whenever a subscription changes (e.g., switching from one plan to -// another, or changing the status from trial to active). -func (c *StripeController) handleCustomerSubscriptionUpdated(event stripe.Event) (ente.StripeEventLog, error) { +// Stripe fires this when a subscription starts or changes. For example, +// renewing a subscription, adding a coupon, applying a discount, adding an +// invoice item, and changing plans all trigger this event. In our case, we use +// this only to track plan changes and renewal failures resulting in +// subscriptions going past due. +func (c *StripeController) handleCustomerSubscriptionUpdated(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) { var stripeSubscription stripe.Subscription json.Unmarshal(event.Data.Raw, &stripeSubscription) currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscription.ID, ente.Stripe) @@ -334,41 +283,40 @@ func (c *StripeController) handleCustomerSubscriptionUpdated(event stripe.Event) } return ente.StripeEventLog{}, stacktrace.Propagate(err, "") } - userID := currentSubscription.UserID - switch stripeSubscription.Status { - case stripe.SubscriptionStatusPastDue: - user, err := c.UserRepo.Get(userID) + newSubscription, err := c.getEnteSubscriptionFromStripeSubscription(userID, stripeSubscription) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + // If the customer has changed the plan, we update state in the database. If + // the plan has not changed, we will ignore this webhook and rely on other + // events to update the state + if currentSubscription.ProductID != newSubscription.ProductID { + c.BillingRepo.ReplaceSubscription(currentSubscription.ID, newSubscription) + } + + fullStripeSub, err := c.getStripeSubscriptionWithPaymentMethod(currentSubscription) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + isSEPA := isSEPASubscription(fullStripeSub) + + if stripeSubscription.Status == stripe.SubscriptionStatusPastDue && !isSEPA { + // Unfortunately, customer.subscription.updated is only fired for SEPA + // payments in case of updation failures (not for purchase or renewal + // failures). So for consistency (and to avoid duplicate mails), we + // trigger on-hold emails for SEPA within handlePaymentIntentFailed. + err = c.sendAccountOnHoldEmail(userID) if err != nil { return ente.StripeEventLog{}, stacktrace.Propagate(err, "") } - err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io", - ente.AccountOnHoldEmailSubject, ente.OnHoldTemplate, map[string]interface{}{ - "PaymentProvider": "Stripe", - }, nil) - if err != nil { - return ente.StripeEventLog{}, stacktrace.Propagate(err, "") - } - case stripe.SubscriptionStatusActive: - newSubscription, err := c.getEnteSubscriptionFromStripeSubscription(userID, stripeSubscription) - if err != nil { - return ente.StripeEventLog{}, stacktrace.Propagate(err, "") - } - if currentSubscription.ProductID == newSubscription.ProductID { - // Webhook is reporting an outdated update that was already verified - // no-op - log.Warn("Webhook is reporting an outdated purchase that was already verified stripeSubscriptionID:", stripeSubscription.ID) - return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil - } - if newSubscription.ProductID != currentSubscription.ProductID { - c.BillingRepo.ReplaceSubscription(currentSubscription.ID, newSubscription) - } } + return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil } // Continue to provision the subscription as payments continue to be made. -func (c *StripeController) handleInvoicePaid(event stripe.Event) (ente.StripeEventLog, error) { +func (c *StripeController) handleInvoicePaid(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) { var invoice stripe.Invoice json.Unmarshal(event.Data.Raw, &invoice) stripeSubscriptionID := invoice.Subscription.ID @@ -404,6 +352,74 @@ func (c *StripeController) handleInvoicePaid(event stripe.Event) (ente.StripeEve return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil } +// Event used to ONLY handle failures to SEPA payments, since we set +// SubscriptionPaymentBehaviorAllowIncomplete only for SEPA. Other payment modes +// will fail and will be handled synchronously +func (c *StripeController) handlePaymentIntentFailed(event stripe.Event, country ente.StripeAccountCountry) (ente.StripeEventLog, error) { + var paymentIntent stripe.PaymentIntent + json.Unmarshal(event.Data.Raw, &paymentIntent) + isSEPA := paymentIntent.LastPaymentError.PaymentMethod.Type == stripe.PaymentMethodTypeSepaDebit + if !isSEPA { + // Ignore events for other payment methods, since they will be handled + // synchronously + log.Info("Ignoring payment intent failed event for non-SEPA payment method") + return ente.StripeEventLog{}, nil + } + + client := c.StripeClients[country] + invoiceID := paymentIntent.Invoice.ID + invoice, err := client.Invoices.Get(invoiceID, nil) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + stripeSubscriptionID := invoice.Subscription.ID + + currentSubscription, err := c.BillingRepo.GetSubscriptionForTransaction(stripeSubscriptionID, ente.Stripe) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // See: Ignore webhooks received before user has been created + log.Warn("Webhook is reporting an event for un-verified subscription stripeSubscriptionID:", stripeSubscriptionID) + } + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + userID := currentSubscription.UserID + + stripeSubscription, err := client.Subscriptions.Get(stripeSubscriptionID, nil) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + + productID := stripeSubscription.Items.Data[0].Price.ID + // If the current subscription is not the same as the one in the webhook, + // then ignore + fmt.Printf("productID: %s, currentSubscription.ProductID: %s\n", productID, currentSubscription.ProductID) + if currentSubscription.ProductID != productID { + // no-op + log.Warn("Webhook is reporting un-verified subscription update", stripeSubscription.ID, "invoiceID:", invoiceID) + return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil + } + // If the current subscription is the same as the one in the webhook, then + // we need to expire the subscription, and send an email to the user. + newExpiryTime := time.Now().UnixMicro() + err = c.BillingRepo.UpdateSubscriptionExpiryTime( + currentSubscription.ID, newExpiryTime) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + + err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, true) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + + err = c.sendAccountOnHoldEmail(userID) + if err != nil { + return ente.StripeEventLog{}, stacktrace.Propagate(err, "") + } + + return ente.StripeEventLog{UserID: userID, StripeSubscription: *stripeSubscription, Event: event}, nil +} + func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (ente.SubscriptionUpdateResponse, error) { subscription, err := c.BillingRepo.GetUserSubscription(userID) if err != nil { @@ -427,11 +443,17 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en log.Info("Usage is good") } - client := c.StripeClients[subscription.Attributes.StripeAccountCountry] - stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, nil) + stripeSubscription, err := c.getStripeSubscriptionWithPaymentMethod(subscription) if err != nil { return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "") } + isSEPA := isSEPASubscription(stripeSubscription) + var paymentBehavior stripe.SubscriptionPaymentBehavior + if isSEPA { + paymentBehavior = stripe.SubscriptionPaymentBehaviorAllowIncomplete + } else { + paymentBehavior = stripe.SubscriptionPaymentBehaviorPendingIfIncomplete + } params := stripe.SubscriptionParams{ ProrationBehavior: stripe.String(string(stripe.SubscriptionProrationBehaviorAlwaysInvoice)), Items: []*stripe.SubscriptionItemsParams{ @@ -440,9 +462,10 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en Price: stripe.String(stripeID), }, }, - PaymentBehavior: stripe.String(string(stripe.SubscriptionPaymentBehaviorPendingIfIncomplete)), + PaymentBehavior: stripe.String(string(paymentBehavior)), } params.AddExpand("latest_invoice.payment_intent") + client := c.StripeClients[subscription.Attributes.StripeAccountCountry] newStripeSubscription, err := client.Subscriptions.Update(subscription.OriginalTransactionID, ¶ms) if err != nil { stripeError := err.(*stripe.Error) @@ -453,19 +476,31 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "") } } - if newStripeSubscription.PendingUpdate != nil { - switch newStripeSubscription.LatestInvoice.PaymentIntent.Status { - case stripe.PaymentIntentStatusRequiresAction: - return ente.SubscriptionUpdateResponse{Status: "requires_action", ClientSecret: newStripeSubscription.LatestInvoice.PaymentIntent.ClientSecret}, nil - case stripe.PaymentIntentStatusRequiresPaymentMethod: - inv := newStripeSubscription.LatestInvoice - invoice.VoidInvoice(inv.ID, nil) - return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil + if isSEPA { + if newStripeSubscription.Status == stripe.SubscriptionStatusPastDue { + if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusRequiresAction { + return ente.SubscriptionUpdateResponse{Status: "requires_action", ClientSecret: newStripeSubscription.LatestInvoice.PaymentIntent.ClientSecret}, nil + } else if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusRequiresPaymentMethod { + return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil + } else if newStripeSubscription.LatestInvoice.PaymentIntent.Status == stripe.PaymentIntentStatusProcessing { + return ente.SubscriptionUpdateResponse{Status: "success"}, nil + } + return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "") + } + } else { + if newStripeSubscription.PendingUpdate != nil { + switch newStripeSubscription.LatestInvoice.PaymentIntent.Status { + case stripe.PaymentIntentStatusRequiresAction: + return ente.SubscriptionUpdateResponse{Status: "requires_action", ClientSecret: newStripeSubscription.LatestInvoice.PaymentIntent.ClientSecret}, nil + case stripe.PaymentIntentStatusRequiresPaymentMethod: + inv := newStripeSubscription.LatestInvoice + client.Invoices.VoidInvoice(inv.ID, nil) + return ente.SubscriptionUpdateResponse{Status: "requires_payment_method"}, nil + } + return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "") } - return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "") } return ente.SubscriptionUpdateResponse{Status: "success"}, nil - } func (c *StripeController) UpdateSubscriptionCancellationStatus(userID int64, status bool) (ente.Subscription, error) { @@ -520,6 +555,29 @@ func (c *StripeController) GetStripeCustomerPortal(userID int64, redirectRootURL return ps.URL, nil } +func (c *StripeController) getStripeSubscriptionWithPaymentMethod(subscription ente.Subscription) (stripe.Subscription, error) { + client := c.StripeClients[subscription.Attributes.StripeAccountCountry] + params := &stripe.SubscriptionParams{} + params.AddExpand("default_payment_method") + stripeSubscription, err := client.Subscriptions.Get(subscription.OriginalTransactionID, params) + if err != nil { + return stripe.Subscription{}, stacktrace.Propagate(err, "") + } + return *stripeSubscription, nil +} + +func (c *StripeController) sendAccountOnHoldEmail(userID int64) error { + user, err := c.UserRepo.Get(userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + err = email.SendTemplatedEmail([]string{user.Email}, "ente", "support@ente.io", + ente.AccountOnHoldEmailSubject, ente.OnHoldTemplate, map[string]interface{}{ + "PaymentProvider": "Stripe", + }, nil) + return err +} + func (c *StripeController) getStripeSubscriptionFromSession(userID int64, checkoutSessionID string) (stripe.Subscription, error) { subscription, err := c.BillingRepo.GetUserSubscription(userID) if err != nil { @@ -619,9 +677,7 @@ func (c *StripeController) CancelSubAndDeleteCustomer(subscription ente.Subscrip if !subscription.Attributes.IsCancelled { prorateRefund := true logger.Info("cancelling sub with prorated refund") - updateParams := &stripe.SubscriptionParams{} - updateParams.AddMetadata(SkipMailKey, "true") - _, err := client.Subscriptions.Update(subscription.OriginalTransactionID, updateParams) + _, err := client.Subscriptions.Update(subscription.OriginalTransactionID, nil) if err != nil { stripeError := err.(*stripe.Error) errorMsg := fmt.Sprintf("subscription updation failed during account deletion: %s, %s", stripeError.Msg, stripeError.Code) @@ -672,34 +728,12 @@ func (c *StripeController) CancelSubAndDeleteCustomer(subscription ente.Subscrip return nil } -// cancel the earlier past_due subscription -// and add skip mail metadata entry to avoid sending account deletion mail while re-subscription -func (c *StripeController) cancelExistingStripeSubscription(subscription ente.Subscription, userID int64) error { - updateParams := &stripe.SubscriptionParams{} - updateParams.AddMetadata(SkipMailKey, "true") - client := c.StripeClients[subscription.Attributes.StripeAccountCountry] - _, err := client.Subscriptions.Update(subscription.OriginalTransactionID, updateParams) - if err != nil { - stripeError := err.(*stripe.Error) - log.Warn(fmt.Sprintf("subscription updation failed msg= %s for userID=%d", stripeError.Msg, userID)) - // ignore if subscription doesn't exist, already deleted - if stripeError.HTTPStatusCode != 404 { - return stacktrace.Propagate(err, "") - } +func isSEPASubscription(stripeSubscription stripe.Subscription) bool { + isSEPA := false + if stripeSubscription.DefaultPaymentMethod != nil { + isSEPA = stripeSubscription.DefaultPaymentMethod.Type == stripe.PaymentMethodTypeSepaDebit } else { - _, err = client.Subscriptions.Cancel(subscription.OriginalTransactionID, nil) - if err != nil { - stripeError := err.(*stripe.Error) - log.Warn(fmt.Sprintf("subscription cancel failed msg= %s for userID=%d", stripeError.Msg, userID)) - // ignore if subscription doesn't exist, already deleted - if stripeError.HTTPStatusCode != 404 { - return stacktrace.Propagate(err, "") - } - } - err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, true) - if err != nil { - return stacktrace.Propagate(err, "") - } + log.Info("No default payment method found") } - return nil + return isSEPA } diff --git a/server/pkg/middleware/rate_limit.go b/server/pkg/middleware/rate_limit.go index 391c01887..08e0f00b6 100644 --- a/server/pkg/middleware/rate_limit.go +++ b/server/pkg/middleware/rate_limit.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" "strings" + "sync/atomic" + "time" "github.com/ente-io/museum/pkg/controller/discord" "github.com/ente-io/museum/pkg/utils/auth" @@ -20,14 +22,40 @@ type RateLimitMiddleware struct { limit10ReqPerMin *limiter.Limiter limit200ReqPerSec *limiter.Limiter discordCtrl *discord.DiscordController + count int64 // Use int64 for atomic operations + limit int64 + reset time.Duration + ticker *time.Ticker } -func NewRateLimitMiddleware(discordCtrl *discord.DiscordController) *RateLimitMiddleware { - return &RateLimitMiddleware{ +func NewRateLimitMiddleware(discordCtrl *discord.DiscordController, limit int64, reset time.Duration) *RateLimitMiddleware { + rl := &RateLimitMiddleware{ limit10ReqPerMin: rateLimiter("10-M"), limit200ReqPerSec: rateLimiter("200-S"), discordCtrl: discordCtrl, + limit: limit, + reset: reset, + ticker: time.NewTicker(reset), } + go func() { + for range rl.ticker.C { + atomic.StoreInt64(&rl.count, 0) // Reset the count every reset interval + } + }() + return rl +} + +// Increment increments the counter in a thread-safe manner. +// Returns true if the increment was within the rate limit, false if the rate limit was exceeded. +func (r *RateLimitMiddleware) Increment() bool { + // Atomically increment the count + newCount := atomic.AddInt64(&r.count, 1) + return newCount <= r.limit +} + +// Stop the internal ticker, effectively stopping the rate limiter. +func (r *RateLimitMiddleware) Stop() { + r.ticker.Stop() } // rateLimiter will return instance of limiter.Limiter based on internal - @@ -44,6 +72,20 @@ func rateLimiter(interval string) *limiter.Limiter { return instance } +// GlobalRateLimiter rate limits all requests to the server, regardless of the endpoint. +func (r *RateLimitMiddleware) GlobalRateLimiter() gin.HandlerFunc { + return func(c *gin.Context) { + if !r.Increment() { + if r.count%100 == 0 { + go r.discordCtrl.NotifyPotentialAbuse(fmt.Sprintf("Global ratelimit (%d) breached %d", r.limit, r.count)) + } + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Rate limit breached, try later"}) + return + } + c.Next() + } +} + // APIRateLimitMiddleware only rate limits sensitive public endpoints which have a higher risk // of abuse by any bad actor. func (r *RateLimitMiddleware) APIRateLimitMiddleware(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { diff --git a/server/pkg/repo/authenticator/entity.go b/server/pkg/repo/authenticator/entity.go index d9a68e84e..056d0043b 100644 --- a/server/pkg/repo/authenticator/entity.go +++ b/server/pkg/repo/authenticator/entity.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/ente-io/museum/ente" model "github.com/ente-io/museum/ente/authenticator" "github.com/ente-io/stacktrace" @@ -43,7 +44,10 @@ func (r *Repository) Get(ctx context.Context, userID int64, id uuid.UUID) (model ) err := row.Scan(&res.ID, &res.UserID, &res.EncryptedData, &res.Header, &res.IsDeleted, &res.CreatedAt, &res.UpdatedAt) if err != nil { - return model.Entity{}, stacktrace.Propagate(err, "failed to getTotpEntry") + if errors.Is(err, sql.ErrNoRows) { + return model.Entity{}, &ente.ErrNotFoundError + } + return model.Entity{}, stacktrace.Propagate(err, "failed to auth entity with id=%s", id) } return res, nil } @@ -70,6 +74,16 @@ func (r *Repository) Update(ctx context.Context, userID int64, req model.UpdateE return stacktrace.Propagate(err, "") } if affected != 1 { + dbEntity, dbEntityErr := r.Get(ctx, userID, req.ID) + if dbEntityErr != nil { + return stacktrace.Propagate(dbEntityErr, fmt.Sprintf("failed to get entity for update with id=%s", req.ID)) + } + if dbEntity.IsDeleted { + return stacktrace.Propagate(ente.NewBadRequestWithMessage("entity is already deleted"), "") + } else if *dbEntity.EncryptedData == req.EncryptedData && *dbEntity.Header == req.Header { + logrus.WithField("id", req.ID).Info("entity is already updated") + return nil + } return stacktrace.Propagate(errors.New("exactly one row should be updated"), "") } return nil diff --git a/server/pkg/repo/embedding/repository.go b/server/pkg/repo/embedding/repository.go index e44753b24..f21e3b4f1 100644 --- a/server/pkg/repo/embedding/repository.go +++ b/server/pkg/repo/embedding/repository.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/lib/pq" "github.com/ente-io/museum/ente" "github.com/ente-io/stacktrace" @@ -18,14 +19,14 @@ type Repository struct { // Create inserts a new embedding -func (r *Repository) InsertOrUpdate(ctx context.Context, ownerID int64, entry ente.InsertOrUpdateEmbeddingRequest) (ente.Embedding, error) { +func (r *Repository) InsertOrUpdate(ctx context.Context, ownerID int64, entry ente.InsertOrUpdateEmbeddingRequest, size int, version int) (ente.Embedding, error) { var updatedAt int64 err := r.DB.QueryRowContext(ctx, `INSERT INTO embeddings - (file_id, owner_id, model) - VALUES ($1, $2, $3) + (file_id, owner_id, model, size, version) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT ON CONSTRAINT unique_embeddings_file_id_model - DO UPDATE SET updated_at = now_utc_micro_seconds() - RETURNING updated_at`, entry.FileID, ownerID, entry.Model).Scan(&updatedAt) + DO UPDATE SET updated_at = now_utc_micro_seconds(), size = $4, version = $5 + RETURNING updated_at`, entry.FileID, ownerID, entry.Model, size, version).Scan(&updatedAt) if err != nil { // check if error is due to model enum invalid value if err.Error() == fmt.Sprintf("pq: invalid input value for enum model: \"%s\"", entry.Model) { @@ -44,7 +45,7 @@ func (r *Repository) InsertOrUpdate(ctx context.Context, ownerID int64, entry en // GetDiff returns the embeddings that have been updated since the given time func (r *Repository) GetDiff(ctx context.Context, ownerID int64, model ente.Model, sinceTime int64, limit int16) ([]ente.Embedding, error) { - rows, err := r.DB.QueryContext(ctx, `SELECT file_id, model, encrypted_embedding, decryption_header, updated_at + rows, err := r.DB.QueryContext(ctx, `SELECT file_id, model, encrypted_embedding, decryption_header, updated_at, version FROM embeddings WHERE owner_id = $1 AND model = $2 AND updated_at > $3 ORDER BY updated_at ASC @@ -55,6 +56,16 @@ func (r *Repository) GetDiff(ctx context.Context, ownerID int64, model ente.Mode return convertRowsToEmbeddings(rows) } +func (r *Repository) GetFilesEmbedding(ctx context.Context, ownerID int64, model ente.Model, fileIDs []int64) ([]ente.Embedding, error) { + rows, err := r.DB.QueryContext(ctx, `SELECT file_id, model, encrypted_embedding, decryption_header, updated_at, version + FROM embeddings + WHERE owner_id = $1 AND model = $2 AND file_id = ANY($3)`, ownerID, model, pq.Array(fileIDs)) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return convertRowsToEmbeddings(rows) +} + func (r *Repository) DeleteAll(ctx context.Context, ownerID int64) error { _, err := r.DB.ExecContext(ctx, "DELETE FROM embeddings WHERE owner_id = $1", ownerID) if err != nil { @@ -82,13 +93,19 @@ func convertRowsToEmbeddings(rows *sql.Rows) ([]ente.Embedding, error) { for rows.Next() { embedding := ente.Embedding{} var encryptedEmbedding, decryptionHeader sql.NullString - err := rows.Scan(&embedding.FileID, &embedding.Model, &encryptedEmbedding, &decryptionHeader, &embedding.UpdatedAt) + var version sql.NullInt32 + err := rows.Scan(&embedding.FileID, &embedding.Model, &encryptedEmbedding, &decryptionHeader, &embedding.UpdatedAt, &version) if encryptedEmbedding.Valid && len(encryptedEmbedding.String) > 0 { embedding.EncryptedEmbedding = encryptedEmbedding.String } if decryptionHeader.Valid && len(decryptionHeader.String) > 0 { embedding.DecryptionHeader = decryptionHeader.String } + v := 1 + if version.Valid { + v = int(version.Int32) + } + embedding.Version = &v if err != nil { return nil, stacktrace.Propagate(err, "") } diff --git a/server/pkg/repo/locationtag/repository.go b/server/pkg/repo/locationtag/repository.go deleted file mode 100644 index 0373ff6cd..000000000 --- a/server/pkg/repo/locationtag/repository.go +++ /dev/null @@ -1,89 +0,0 @@ -package locationtag - -import ( - "context" - "database/sql" - "fmt" - "github.com/ente-io/museum/ente" - "github.com/ente-io/stacktrace" - "github.com/google/uuid" - "github.com/sirupsen/logrus" -) - -// Repository defines the methods for inserting, updating and retrieving -// locationTag related entities from the underlying repository -type Repository struct { - DB *sql.DB -} - -// Create inserts a new &{ente.LocationTag} entry -func (r *Repository) Create(ctx context.Context, locationTag ente.LocationTag) (ente.LocationTag, error) { - err := r.DB.QueryRow(`INSERT into location_tag( - id, - user_id, - encrypted_key, - key_decryption_nonce, - attributes) VALUES ($1,$2,$3,$4,$5) RETURNING id,created_at,updated_at`, - uuid.New(), //$1 id - locationTag.OwnerID, // $2 user_id - locationTag.EncryptedKey, // $3 encrypted_key - locationTag.KeyDecryptionNonce, // $4 key_decryption_nonce - locationTag.Attributes). // %5 attributes - Scan(&locationTag.ID, &locationTag.CreatedAt, &locationTag.UpdatedAt) - if err != nil { - return ente.LocationTag{}, stacktrace.Propagate(err, "Failed to create locationTag") - } - return locationTag, nil -} - -// GetDiff returns the &{[]ente.LocationTag} which have been added or -// modified after the given sinceTime -func (r *Repository) GetDiff(ctx context.Context, ownerID int64, sinceTime int64, limit int16) ([]ente.LocationTag, error) { - rows, err := r.DB.Query(`SELECT - id, user_id, provider, encrypted_key, key_decryption_nonce, - attributes, is_deleted, created_at, updated_at - FROM location_tag - WHERE user_id = $1 - and updated_at > $2 - ORDER BY updated_at - LIMIT $3`, - ownerID, // $1 - sinceTime, // %2 - limit, // $3 - ) - if err != nil { - return nil, stacktrace.Propagate(err, "GetDiff query failed") - } - return convertRowsToLocationTags(rows) -} - -func (r *Repository) Delete(ctx context.Context, id string, ownerID int64) (bool, error) { - _, err := r.DB.ExecContext(ctx, - `UPDATE location_tag SET is_deleted=$1, attributes=$2 where id=$3 and user_id = $4`, - true, `{}`, // $1 is_deleted, $2 attr - id, ownerID) // $3 tagId, $4 ownerID - if err != nil { - return false, stacktrace.Propagate(err, fmt.Sprintf("faield to delele tag with id=%s", id)) - } - return true, nil -} - -func convertRowsToLocationTags(rows *sql.Rows) ([]ente.LocationTag, error) { - defer func() { - if err := rows.Close(); err != nil { - logrus.Error(err) - } - }() - locationTags := make([]ente.LocationTag, 0) - for rows.Next() { - tag := ente.LocationTag{} - err := rows.Scan( - &tag.ID, &tag.OwnerID, &tag.Provider, &tag.EncryptedKey, &tag.KeyDecryptionNonce, - &tag.Attributes, &tag.IsDeleted, &tag.CreatedAt, &tag.UpdatedAt) - if err != nil { - return nil, stacktrace.Propagate(err, "Failed to convert rowToLocationTag") - } - locationTags = append(locationTags, tag) - } - return locationTags, nil -} diff --git a/server/pkg/utils/array/array.go b/server/pkg/utils/array/array.go index 98869ad5d..42b6b8fa9 100644 --- a/server/pkg/utils/array/array.go +++ b/server/pkg/utils/array/array.go @@ -47,3 +47,27 @@ func Int64InList(a int64, list []int64) bool { } return false } + +// FindMissingElementsInSecondList identifies elements in 'sourceList' that are not present in 'targetList'. +// Returns: +// - A slice of int64 representing the elements found in 'sourceList' but not in 'targetList'. +// If all elements of 'sourceList' are present in 'targetList', an empty slice is returned. +// +// Example usage: +// missingElements := FindMissingElementsInSecondList([]int64{1, 2, 3, 4}, []int64{2, 4, 6}) +// fmt.Println(missingElements) // Output: [1, 3] +func FindMissingElementsInSecondList(sourceList []int64, targetList []int64) []int64 { + targetSet := make(map[int64]struct{}) + for _, item := range targetList { + targetSet[item] = struct{}{} + } + + var missingElements = make([]int64, 0) + for _, item := range sourceList { + if _, found := targetSet[item]; !found { + missingElements = append(missingElements, item) + } + } + + return missingElements +} diff --git a/server/pkg/utils/email/email.go b/server/pkg/utils/email/email.go index 89993882c..46202313e 100644 --- a/server/pkg/utils/email/email.go +++ b/server/pkg/utils/email/email.go @@ -10,6 +10,7 @@ import ( "encoding/json" "html/template" "net/http" + "net/smtp" "strings" "github.com/ente-io/museum/ente" @@ -20,6 +21,78 @@ import ( // Send sends an email func Send(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { + smtpHost := viper.GetString("smtp.host") + if smtpHost != "" { + return sendViaSMTP(toEmails, fromName, fromEmail, subject, htmlBody, inlineImages) + } else { + return sendViaTransmail(toEmails, fromName, fromEmail, subject, htmlBody, inlineImages) + } +} + +func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { + if len(toEmails) == 0 { + return ente.ErrBadRequest + } + + smtpServer := viper.GetString("smtp.host") + smtpPort := viper.GetString("smtp.port") + smtpUsername := viper.GetString("smtp.username") + smtpPassword := viper.GetString("smtp.password") + + var emailMessage string + + // Construct 'emailAddresses' with comma-separated email addresses + var emailAddresses string + for i, email := range toEmails { + if i != 0 { + emailAddresses += "," + } + emailAddresses += email + } + + header := "From: " + fromName + " <" + fromEmail + ">\n" + + "To: " + emailAddresses + "\n" + + "Subject: " + subject + "\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/related; boundary=boundary\n\n" + + "--boundary\n" + htmlContent := "Content-Type: text/html; charset=us-ascii\n\n" + htmlBody + "\n" + + emailMessage = header + htmlContent + + if inlineImages == nil { + emailMessage += "--boundary--" + } else { + for _, inlineImage := range inlineImages { + + emailMessage += "--boundary\n" + var mimeType = inlineImage["mime_type"].(string) + var contentID = inlineImage["cid"].(string) + var imgBase64Str = inlineImage["content"].(string) + + var image = "Content-Type: " + mimeType + "\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-ID: <" + contentID + ">\n" + + "Content-Disposition: inline\n\n" + imgBase64Str + "\n" + + emailMessage += image + } + emailMessage += "--boundary--" + } + + // Send the email to each recipient + for _, toEmail := range toEmails { + auth := smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer) + err := smtp.SendMail(smtpServer+":"+smtpPort, auth, fromEmail, []string{toEmail}, []byte(emailMessage)) + if err != nil { + return stacktrace.Propagate(err, "") + } + } + + return nil +} + +func sendViaTransmail(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { if len(toEmails) == 0 { return ente.ErrBadRequest } @@ -69,6 +142,7 @@ func SendTemplatedEmail(to []string, fromName string, fromEmail string, subject if err != nil { return stacktrace.Propagate(err, "") } + return Send(to, fromName, fromEmail, subject, body, inlineImages) } diff --git a/server/pkg/utils/s3config/s3config.go b/server/pkg/utils/s3config/s3config.go index fe83ce6ea..9b273bd61 100644 --- a/server/pkg/utils/s3config/s3config.go +++ b/server/pkg/utils/s3config/s3config.go @@ -104,6 +104,7 @@ func (config *S3Config) initialize() { config.s3Configs = make(map[string]*aws.Config) config.s3Clients = make(map[string]s3.S3) + usePathStyleURLs := viper.GetBool("s3.use_path_style_urls") areLocalBuckets := viper.GetBool("s3.are_local_buckets") config.areLocalBuckets = areLocalBuckets @@ -116,6 +117,9 @@ func (config *S3Config) initialize() { Endpoint: aws.String(viper.GetString("s3." + dc + ".endpoint")), Region: aws.String(viper.GetString("s3." + dc + ".region")), } + if usePathStyleURLs { + s3Config.S3ForcePathStyle = aws.Bool(true) + } if areLocalBuckets { s3Config.DisableSSL = aws.Bool(true) s3Config.S3ForcePathStyle = aws.Bool(true) diff --git a/server/scripts/deploy/README.md b/server/scripts/deploy/README.md new file mode 100644 index 000000000..35e1ec079 --- /dev/null +++ b/server/scripts/deploy/README.md @@ -0,0 +1,99 @@ +# Production Deployments + +This document outlines how we ourselves deploy museum. Note that this is very +specific to our use case, and while this might be useful as an example, this is +likely overkill for simple self hosted deployments. + +## Overview + +We use museum's Dockerfile to build images which we then run on vanilla Ubuntu +servers (+ Docker installed). For ease of administration, we wrap Docker +commands to start/stop/update it in a systemd service. + +* The production machines are vanilla Ubuntu instances, with Docker and Promtail +installed. + +* There is a [GitHub action](../../../.github/workflows/server-release.yml) to + build museum Docker images using its Dockerfile. + +* We wrap the commands to start and stop containers using these images in a + systemd service. + +* We call this general concept of standalone Docker images that are managed +using systemd as "services". More examples and details +[here](../../../infra/services/README.md). + +* So museum is a "service". You can see its systemd unit definition in + [museum.service](museum.service) + +* On the running instance, we use `systemctl start|stop|status museum` to manage + it. + +* The service automatically updates itself on each start. There's also a + convenience [script](update-and-restart-museum.sh) that pre-downloads the + latest image to further reduce the delay during a restart. + +* Optionally and alternatively, museum can also be run behind an Nginx. This + option has a separate service definition. + +## Installation + +To bring up an additional museum node: + +* Prepare the instance to run our services + +* Setup [promtail](../../../infra/services/promtail/README.md), [prometheus and + node-exporter](../../../infra/services/prometheus/README.md) services + +* If running behind Nginx, install the + [nginx](../../../infra/services/nginx/README.md) service. + +* Add credentials + + sudo mkdir -p /root/museum/credentials + sudo tee /root/museum/credentials/pst-service-account.json + sudo tee /root/museum/credentials/fcm-service-account.json + sudo tee /root/museum/credentials.yaml + +* Add billing data + + scp /path/to/billing/*.json : + + sudo mkdir -p /root/museum/data/billing + sudo mv *.json /root/museum/data/billing/ + +* If not running behind Nginx, add the TLS credentials (otherwise add the to + Nginx) + + sudo tee /root/museum/credentials/tls.cert + sudo tee /root/museum/credentials/tls.key + +* Copy the service definition and restart script to the new instance. The + restart script can remain in the ente user's home directory. Move the service + definition to its proper place. + + # If using nginx + scp scripts/deploy/museum.nginx.service :museum.service + # otherwise + scp scripts/deploy/museum.service : + + scp scripts/deploy/update-and-restart-museum.sh : + + sudo mv museum.service /etc/systemd/system + sudo systemctl daemon-reload + +* If running behind Nginx, tell it about museum + + scp scripts/deploy/museum.nginx.conf : + + sudo mv museum.nginx.conf /root/nginx/conf.d + sudo systemctl reload nginx + +## Starting + +SSH into the instance, and run + + ./update-and-restart-museum.sh + +This'll ask for sudo credentials, pull the latest Docker image, restart the +museum service and start tailing the logs (as a sanity check). diff --git a/server/scripts/deploy/museum.nginx.conf b/server/scripts/deploy/museum.nginx.conf new file mode 100644 index 000000000..65ed19b49 --- /dev/null +++ b/server/scripts/deploy/museum.nginx.conf @@ -0,0 +1,24 @@ +# This file gets loaded in a top level http block by the default nginx.conf +# See infra/services/nginx/README.md for more details. + +upstream museum { + # https://nginx.org/en/docs/http/ngx_http_upstream_module.html + server host.docker.internal:8080 max_conns=50; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + server_name api.ente.io; + + location / { + proxy_pass http://museum; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/server/scripts/deploy/museum.nginx.service b/server/scripts/deploy/museum.nginx.service new file mode 100644 index 000000000..3a8b0b653 --- /dev/null +++ b/server/scripts/deploy/museum.nginx.service @@ -0,0 +1,23 @@ +[Unit] +Documentation=https://github.com/ente-io/ente/tree/main/server#readme +Requires=docker.service +After=docker.service +Requires=nginx.service +After=nginx.service + +[Service] +Restart=on-failure +ExecStartPre=docker pull rg.fr-par.scw.cloud/ente/museum-prod +ExecStartPre=-docker stop museum +ExecStartPre=-docker rm museum +ExecStart=docker run --name museum \ + -e ENVIRONMENT=production \ + -e ENTE_HTTP_USE-TLS=0 \ + --hostname "%H" \ + -p 8080:8080 \ + -p 2112:2112 \ + -v /root/museum/credentials:/credentials:ro \ + -v /root/museum/credentials.yaml:/credentials.yaml:ro \ + -v /root/museum/data:/data:ro \ + -v /root/var:/var \ + rg.fr-par.scw.cloud/ente/museum-prod diff --git a/server/scripts/museum.service b/server/scripts/deploy/museum.service similarity index 76% rename from server/scripts/museum.service rename to server/scripts/deploy/museum.service index 6f66133fe..470add934 100644 --- a/server/scripts/museum.service +++ b/server/scripts/deploy/museum.service @@ -1,10 +1,7 @@ [Unit] -Documentation=https://github.com/ente-io/museum +Documentation=https://github.com/ente-io/ente/tree/main/server#readme Requires=docker.service After=docker.service -# Don't automatically restart if it fails more than 5 times in 10 minutes. -StartLimitIntervalSec=600 -StartLimitBurst=5 [Service] Restart=on-failure diff --git a/server/scripts/deploy/update-and-restart-museum.sh b/server/scripts/deploy/update-and-restart-museum.sh new file mode 100755 index 000000000..85f93ae05 --- /dev/null +++ b/server/scripts/deploy/update-and-restart-museum.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# This script is meant to be run on the production instances. +# +# It will pull the latest Docker image, restart the museum process and start +# tailing the logs as a sanity check. + +set -o errexit + +# The service file also does this, but also pre-pull here to minimize downtime. +sudo docker pull rg.fr-par.scw.cloud/ente/museum-prod + +sudo systemctl restart museum +sudo systemctl status museum | more +sudo tail -f /root/var/logs/museum.log diff --git a/web/.gitignore b/web/.gitignore index f07d93259..afc0a0b1d 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -8,8 +8,7 @@ node_modules/ .vscode/ # Local env files -.env -.env.*.local +.env*.local # Next.js .next/ diff --git a/web/README.md b/web/README.md index 36eb1fb25..908676c55 100644 --- a/web/README.md +++ b/web/README.md @@ -32,7 +32,7 @@ yarn dev That's it. The web app will automatically hot reload when you make changes. -If you're new to web development and unsure about how to get started, or are +If you're new to web development and unsure about how to get started, or are facing some problems when running the above steps, see [docs/new](docs/new.md). ## Other apps diff --git a/web/apps/accounts/public/locales/bg-BG/translation.json b/web/apps/accounts/public/locales/bg-BG/translation.json new file mode 100644 index 000000000..03faf16c2 --- /dev/null +++ b/web/apps/accounts/public/locales/bg-BG/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "
Личен бекъп
на твоите спомени
", + "HERO_SLIDE_1": "Криптиран от край до край по подразбиране", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/accounts/public/locales/de-DE/translation.json b/web/apps/accounts/public/locales/de-DE/translation.json index c0333d131..9a1c4073c 100644 --- a/web/apps/accounts/public/locales/de-DE/translation.json +++ b/web/apps/accounts/public/locales/de-DE/translation.json @@ -31,80 +31,78 @@ "VERIFY_PASSPHRASE": "Einloggen", "INCORRECT_PASSPHRASE": "Falsches Passwort", "ENTER_ENC_PASSPHRASE": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", - "PASSPHRASE_DISCLAIMER": "", + "PASSPHRASE_DISCLAIMER": "Wir speichern dein Passwort nicht. Wenn du es vergisst, können wir dir nicht helfen, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", "WELCOME_TO_ENTE_HEADING": "Willkommen bei
", - "WELCOME_TO_ENTE_SUBHEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Wo deine besten Fotos leben", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...", "PASSPHRASE_HINT": "Passwort", "CONFIRM_PASSPHRASE": "Passwort bestätigen", - "REFERRAL_CODE_HINT": "", - "REFERRAL_INFO": "", + "REFERRAL_CODE_HINT": "Wie hast du von Ente erfahren? (optional)", + "REFERRAL_INFO": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein", - "CONSOLE_WARNING_STOP": "STOPP!", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "Neues Album", "ENTER_ALBUM_NAME": "Albumname", "CLOSE_OPTION": "Schließen (Esc)", "ENTER_FILE_NAME": "Dateiname", "CLOSE": "Schließen", "NO": "Nein", - "NOTHING_HERE": "", + "NOTHING_HERE": "Hier gibt es noch nichts zu sehen 👀", "UPLOAD": "Hochladen", "IMPORT": "Importieren", "ADD_PHOTOS": "Fotos hinzufügen", "ADD_MORE_PHOTOS": "Mehr Fotos hinzufügen", - "add_photos_one": "", - "add_photos_other": "", + "add_photos_one": "Eine Datei hinzufügen", + "add_photos_other": "{{count, number}} Dateien hinzufügen", "SELECT_PHOTOS": "Foto auswählen", "FILE_UPLOAD": "Datei hochladen", "UPLOAD_STAGE_MESSAGE": { "0": "Hochladen wird vorbereitet", - "1": "", - "2": "", - "3": "", - "4": "", + "1": "Lese Google-Metadaten", + "2": "Metadaten von {{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien extrahiert", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien verarbeitet", + "4": "Verbleibende Uploads werden abgebrochen", "5": "Sicherung abgeschlossen" }, - "FILE_NOT_UPLOADED_LIST": "", + "FILE_NOT_UPLOADED_LIST": "Die folgenden Dateien wurden nicht hochgeladen", "SUBSCRIPTION_EXPIRED": "Abonnement abgelaufen", - "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Dein Abonnement ist abgelaufen, bitte erneuere es", "STORAGE_QUOTA_EXCEEDED": "Speichergrenze überschritten", - "INITIAL_LOAD_DELAY_WARNING": "", - "USER_DOES_NOT_EXIST": "", - "NO_ACCOUNT": "", - "ACCOUNT_EXISTS": "", + "INITIAL_LOAD_DELAY_WARNING": "Das erste Laden kann einige Zeit in Anspruch nehmen", + "USER_DOES_NOT_EXIST": "Leider konnte kein Benutzer mit dieser E-Mail gefunden werden", + "NO_ACCOUNT": "Kein Konto vorhanden", + "ACCOUNT_EXISTS": "Es ist bereits ein Account vorhanden", "CREATE": "Erstellen", "DOWNLOAD": "Herunterladen", "DOWNLOAD_OPTION": "Herunterladen (D)", "DOWNLOAD_FAVORITES": "Favoriten herunterladen", - "DOWNLOAD_UNCATEGORIZED": "", - "DOWNLOAD_HIDDEN_ITEMS": "", + "DOWNLOAD_UNCATEGORIZED": "Download unkategorisiert", + "DOWNLOAD_HIDDEN_ITEMS": "Versteckte Dateien herunterladen", "COPY_OPTION": "Als PNG kopieren (Strg / Cmd - C)", - "TOGGLE_FULLSCREEN": "", + "TOGGLE_FULLSCREEN": "Vollbild umschalten (F)", "ZOOM_IN_OUT": "Herein-/Herauszoomen", - "PREVIOUS": "", - "NEXT": "", - "TITLE_PHOTOS": "", - "TITLE_ALBUMS": "", - "TITLE_AUTH": "", + "PREVIOUS": "Vorherige (←)", + "NEXT": "Weitere (→)", + "TITLE_PHOTOS": "Ente Fotos", + "TITLE_ALBUMS": "Ente Fotos", + "TITLE_AUTH": "Ente Auth", "UPLOAD_FIRST_PHOTO": "Lade dein erstes Foto hoch", "IMPORT_YOUR_FOLDERS": "Importiere deiner Ordner", - "UPLOAD_DROPZONE_MESSAGE": "", - "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "UPLOAD_DROPZONE_MESSAGE": "Loslassen, um Dateien zu sichern", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Loslassen, um beobachteten Ordner hinzuzufügen", "TRASH_FILES_TITLE": "Dateien löschen?", "TRASH_FILE_TITLE": "Datei löschen?", "DELETE_FILES_TITLE": "Sofort löschen?", - "DELETE_FILES_MESSAGE": "", + "DELETE_FILES_MESSAGE": "Ausgewählte Dateien werden dauerhaft aus Ihrem Ente-Konto gelöscht.", "DELETE": "Löschen", "DELETE_OPTION": "Löschen (DEL)", - "FAVORITE_OPTION": "", - "UNFAVORITE_OPTION": "", - "MULTI_FOLDER_UPLOAD": "", - "UPLOAD_STRATEGY_CHOICE": "", + "FAVORITE_OPTION": "Zu Favoriten hinzufügen (L)", + "UNFAVORITE_OPTION": "Von Favoriten entfernen (L)", + "MULTI_FOLDER_UPLOAD": "Mehrere Ordner erkannt", + "UPLOAD_STRATEGY_CHOICE": "Möchtest du sie hochladen in", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Ein einzelnes Album", "OR": "oder", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Getrennte Alben", "SESSION_EXPIRED_MESSAGE": "Ihre Sitzung ist abgelaufen. Bitte loggen Sie sich erneut ein, um fortzufahren", "SESSION_EXPIRED": "Sitzung abgelaufen", "PASSWORD_GENERATION_FAILED": "Dein Browser konnte keinen starken Schlüssel generieren, der den Verschlüsselungsstandards des Entes entspricht, bitte versuche die mobile App oder einen anderen Browser zu verwenden", @@ -113,9 +111,9 @@ "RECOVERY_KEY": "Wiederherstellungsschlüssel", "SAVE_LATER": "Auf später verschieben", "SAVE": "Schlüssel speichern", - "RECOVERY_KEY_DESCRIPTION": "", - "RECOVER_KEY_GENERATION_FAILED": "", - "KEY_NOT_STORED_DISCLAIMER": "", + "RECOVERY_KEY_DESCRIPTION": "Falls du dein Passwort vergisst, kannst du deine Daten nur mit diesem Schlüssel wiederherstellen.", + "RECOVER_KEY_GENERATION_FAILED": "Wiederherstellungsschlüssel konnte nicht generiert werden, bitte versuche es erneut", + "KEY_NOT_STORED_DISCLAIMER": "Wir speichern diesen Schlüssel nicht, also speichere ihn bitte an einem sicheren Ort", "FORGOT_PASSWORD": "Passwort vergessen", "RECOVER_ACCOUNT": "Konto wiederherstellen", "RECOVERY_KEY_HINT": "Wiederherstellungsschlüssel", @@ -132,15 +130,15 @@ "CANCEL": "Abbrechen", "LOGOUT": "Ausloggen", "DELETE_ACCOUNT": "Konto löschen", - "DELETE_ACCOUNT_MESSAGE": "", + "DELETE_ACCOUNT_MESSAGE": "

Bitte sende eine E-Mail an {{emailID}} mit deiner registrierten E-Mail-Adresse.

Deine Anfrage wird innerhalb von 72 Stunden bearbeitet.

", "LOGOUT_MESSAGE": "Sind sie sicher, dass sie sich ausloggen möchten?", "CHANGE_EMAIL": "E-Mail-Adresse ändern", "OK": "OK", "SUCCESS": "Erfolgreich", "ERROR": "Fehler", "MESSAGE": "Nachricht", - "INSTALL_MOBILE_APP": "", - "DOWNLOAD_APP_MESSAGE": "", + "INSTALL_MOBILE_APP": "Installiere unsere Android oder iOS App, um automatisch alle deine Fotos zu sichern", + "DOWNLOAD_APP_MESSAGE": "Entschuldigung, dieser Vorgang wird derzeit nur von unserer Desktop-App unterstützt", "DOWNLOAD_APP": "Desktopanwendung herunterladen", "EXPORT": "Daten exportieren", "SUBSCRIPTION": "Abonnement", @@ -154,37 +152,37 @@ "MANAGE_PLAN": "Verwalte dein Abonnement", "ACTIVE": "Aktiv", "OFFLINE_MSG": "Du bist offline, gecachte Erinnerungen werden angezeigt", - "FREE_SUBSCRIPTION_INFO": "", + "FREE_SUBSCRIPTION_INFO": "Du bist auf dem kostenlosen Plan, der am {{date, dateTime}} ausläuft", "FAMILY_SUBSCRIPTION_INFO": "Sie haben einen Familienplan verwaltet von", "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Erneuert am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Endet am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Ihr Abo endet am {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "", + "ADD_ON_AVAILABLE_TILL": "Dein {{storage, string}} Add-on ist gültig bis {{date, dateTime}}", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Sie haben Ihr Speichervolumen überschritten, bitte upgraden Sie", - "SUBSCRIPTION_PURCHASE_SUCCESS": "", - "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "

Wir haben deine Zahlung erhalten

Dein Abonnement ist gültig bis {{date, dateTime}}

", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Dein Kauf wurde abgebrochen. Bitte versuche es erneut, wenn du abonnieren willst", "SUBSCRIPTION_PURCHASE_FAILED": "Kauf des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", "SUBSCRIPTION_UPDATE_FAILED": "Aktualisierung des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", - "UPDATE_PAYMENT_METHOD_MESSAGE": "", - "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Es tut uns leid, die Zahlung ist fehlgeschlagen, als wir versuchten Ihre Karte zu belasten. Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut", + "STRIPE_AUTHENTICATION_FAILED": "Wir können deine Zahlungsmethode nicht authentifizieren. Bitte wähle eine andere Zahlungsmethode und versuche es erneut", "UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren", "MONTHLY": "Monatlich", "YEARLY": "Jährlich", "UPDATE_SUBSCRIPTION_MESSAGE": "Sind Sie sicher, dass Sie Ihren Tarif ändern möchten?", "UPDATE_SUBSCRIPTION": "Plan ändern", "CANCEL_SUBSCRIPTION": "Abonnement kündigen", - "CANCEL_SUBSCRIPTION_MESSAGE": "", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", - "SUBSCRIPTION_CANCEL_FAILED": "", - "SUBSCRIPTION_CANCEL_SUCCESS": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "

Alle deine Daten werden am Ende dieses Abrechnungszeitraums von unseren Servern gelöscht.

Bist du sicher, dass du dein Abonnement kündigen möchtest?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Bist du sicher, dass du dein Abonnement beenden möchtest?

", + "SUBSCRIPTION_CANCEL_FAILED": "Abonnement konnte nicht storniert werden", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement erfolgreich beendet", "REACTIVATE_SUBSCRIPTION": "Abonnement reaktivieren", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "", - "SUBSCRIPTION_ACTIVATE_FAILED": "", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", - "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Nach der Reaktivierung wird am {{date, dateTime}} abgerechnet", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Abonnement erfolgreich aktiviert ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Reaktivierung der Abonnementverlängerung fehlgeschlagen", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Vielen Dank", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Mobiles Abonnement kündigen", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Bitte kündige dein Abonnement in der mobilen App, um hier ein Abonnement zu aktivieren", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Bitte kontaktiere uns über {{emailID}}, um dein Abo zu verwalten", "RENAME": "Umbenennen", "RENAME_FILE": "Datei umbenennen", "RENAME_COLLECTION": "Album umbenennen", @@ -198,43 +196,43 @@ "SHAREES": "Geteilt mit", "SHARE_WITH_SELF": "Du kannst nicht mit dir selbst teilen", "ALREADY_SHARED": "Hoppla, Sie teilen dies bereits mit {{email}}", - "SHARING_BAD_REQUEST_ERROR": "", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "SHARING_BAD_REQUEST_ERROR": "Albumfreigabe nicht erlaubt", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Freigabe ist für kostenlose Konten deaktiviert", "DOWNLOAD_COLLECTION": "Album herunterladen", - "DOWNLOAD_COLLECTION_MESSAGE": "", - "CREATE_ALBUM_FAILED": "", + "DOWNLOAD_COLLECTION_MESSAGE": "

Bist du sicher, dass du das komplette Album herunterladen möchtest?

Alle Dateien werden der Warteschlange zum sequenziellen Download hinzugefügt

", + "CREATE_ALBUM_FAILED": "Fehler beim Erstellen des Albums, bitte versuche es erneut", "SEARCH": "Suchen", "SEARCH_RESULTS": "Ergebnisse durchsuchen", - "NO_RESULTS": "", - "SEARCH_HINT": "", + "NO_RESULTS": "Keine Ergebnisse gefunden", + "SEARCH_HINT": "Suche nach Alben, Datum, Beschreibungen, ...", "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Standort", - "CITY": "", + "CITY": "Ort", "DATE": "Datum", "FILE_NAME": "Dateiname", "THING": "Inhalt", "FILE_CAPTION": "Beschreibung", - "FILE_TYPE": "", - "CLIP": "" + "FILE_TYPE": "Dateityp", + "CLIP": "Magie" }, "photos_count_zero": "Keine Erinnerungen", - "photos_count_one": "", - "photos_count_other": "", - "TERMS_AND_CONDITIONS": "", + "photos_count_one": "Eine Erinnerung", + "photos_count_other": "{{count, number}} Erinnerungen", + "TERMS_AND_CONDITIONS": "Ich stimme den Bedingungen und Datenschutzrichtlinien zu", "ADD_TO_COLLECTION": "Zum Album hinzufügen", - "SELECTED": "", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "SELECTED": "ausgewählt", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Dieses Video kann in deinem Browser nicht abgespielt werden", "PEOPLE": "Personen", - "INDEXING_SCHEDULED": "", - "ANALYZING_PHOTOS": "", - "INDEXING_PEOPLE": "", - "INDEXING_DONE": "", - "UNIDENTIFIED_FACES": "", - "OBJECTS": "", - "TEXT": "", + "INDEXING_SCHEDULED": "Indizierung ist geplant...", + "ANALYZING_PHOTOS": "Indiziere Fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Indiziere Personen in {{indexStatus.nSyncedFiles,number}} Fotos...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles,number}} Fotos wurden indiziert", + "UNIDENTIFIED_FACES": "unidentifizierte Gesichter", + "OBJECTS": "Objekte", + "TEXT": "Text", "INFO": "Info ", - "INFO_OPTION": "", + "INFO_OPTION": "Info (I)", "FILE_NAME": "Dateiname", "CAPTION_PLACEHOLDER": "Eine Beschreibung hinzufügen", "LOCATION": "Standort", @@ -244,8 +242,8 @@ "ENABLE_MAPS": "Karten aktivieren?", "ENABLE_MAP": "Karte aktivieren", "DISABLE_MAPS": "Karten deaktivieren?", - "ENABLE_MAP_DESCRIPTION": "", - "DISABLE_MAP_DESCRIPTION": "", + "ENABLE_MAP_DESCRIPTION": "

Dies wird deine Fotos auf einer Weltkarte anzeigen.

Die Karte wird von OpenStreetMap gehostet und die genauen Standorte deiner Fotos werden niemals geteilt.

Diese Funktion kannst du jederzeit in den Einstellungen deaktivieren.

", + "DISABLE_MAP_DESCRIPTION": "

Dies wird die Anzeige deiner Fotos auf einer Weltkarte deaktivieren.

Du kannst diese Funktion jederzeit in den Einstellungen aktivieren.

", "DISABLE_MAP": "Karte deaktivieren", "DETAILS": "Details", "VIEW_EXIF": "Alle EXIF-Daten anzeigen", @@ -254,67 +252,67 @@ "ISO": "ISO", "TWO_FACTOR": "Zwei-Faktor", "TWO_FACTOR_AUTHENTICATION": "Zwei-Faktor-Authentifizierung", - "TWO_FACTOR_QR_INSTRUCTION": "", + "TWO_FACTOR_QR_INSTRUCTION": "Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App", "ENTER_CODE_MANUALLY": "Geben Sie den Code manuell ein", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Bitte gib diesen Code in deiner bevorzugten Authentifizierungs-App ein", "SCAN_QR_CODE": "QR‐Code stattdessen scannen", "ENABLE_TWO_FACTOR": "Zwei-Faktor-Authentifizierung aktivieren", "ENABLE": "Aktivieren", - "LOST_DEVICE": "", + "LOST_DEVICE": "Zwei-Faktor-Gerät verloren", "INCORRECT_CODE": "Falscher Code", - "TWO_FACTOR_INFO": "", + "TWO_FACTOR_INFO": "Fügen Sie eine zusätzliche Sicherheitsebene hinzu, indem Sie mehr als Ihre E-Mail und Ihr Passwort benötigen, um sich mit Ihrem Account anzumelden", "DISABLE_TWO_FACTOR_LABEL": "Deaktiviere die Zwei-Faktor-Authentifizierung", - "UPDATE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "Authentifizierungsgerät aktualisieren", "DISABLE": "Deaktivieren", "RECONFIGURE": "Neu einrichten", - "UPDATE_TWO_FACTOR": "", - "UPDATE_TWO_FACTOR_MESSAGE": "", - "UPDATE": "", - "DISABLE_TWO_FACTOR": "", - "DISABLE_TWO_FACTOR_MESSAGE": "", - "TWO_FACTOR_DISABLE_FAILED": "", + "UPDATE_TWO_FACTOR": "Zweiten Faktor aktualisieren", + "UPDATE_TWO_FACTOR_MESSAGE": "Fahren Sie fort, werden alle Ihre zuvor konfigurierten Authentifikatoren ungültig", + "UPDATE": "Aktualisierung", + "DISABLE_TWO_FACTOR": "Zweiten Faktor deaktivieren", + "DISABLE_TWO_FACTOR_MESSAGE": "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst", + "TWO_FACTOR_DISABLE_FAILED": "Fehler beim Deaktivieren des zweiten Faktors, bitte versuchen Sie es erneut", "EXPORT_DATA": "Daten exportieren", "SELECT_FOLDER": "Ordner auswählen", "DESTINATION": "Zielort", "START": "Start", - "LAST_EXPORT_TIME": "", - "EXPORT_AGAIN": "", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "LAST_EXPORT_TIME": "Letztes Exportdatum", + "EXPORT_AGAIN": "Neusynchronisation", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", "SEND_OTT": "OTP senden", "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", "ETAGS_BLOCKED": "", "SKIPPED_VIDEOS_INFO": "", "LIVE_PHOTOS_DETECTED": "", - "RETRY_FAILED": "", + "RETRY_FAILED": "Fehlgeschlagene Uploads erneut probieren", "FAILED_UPLOADS": "Fehlgeschlagene Uploads ", "SKIPPED_FILES": "Ignorierte Uploads", "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Das Vorschaubild konnte nicht erzeugt werden", "UNSUPPORTED_FILES": "Nicht unterstützte Dateien", - "SUCCESSFUL_UPLOADS": "", + "SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads", "SKIPPED_INFO": "", "UNSUPPORTED_INFO": "ente unterstützt diese Dateiformate noch nicht", "BLOCKED_UPLOADS": "Blockierte Uploads", "SKIPPED_VIDEOS": "Übersprungene Videos", "INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung", - "INPROGRESS_UPLOADS": "", + "INPROGRESS_UPLOADS": "Upload läuft", "TOO_LARGE_UPLOADS": "Große Dateien", "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Zu wenig Speicher", "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie die maximale Größe für Ihren Speicherplan überschreiten", "TOO_LARGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", "THUMBNAIL_GENERATION_FAILED_INFO": "Diese Dateien wurden hochgeladen, aber leider konnten wir nicht die Thumbnails für sie generieren.", "UPLOAD_TO_COLLECTION": "In Album hochladen", - "UNCATEGORIZED": "", + "UNCATEGORIZED": "Unkategorisiert", "ARCHIVE": "Archiv", "FAVORITES": "Favoriten", "ARCHIVE_COLLECTION": "Album archivieren", "ARCHIVE_SECTION_NAME": "Archiv", "ALL_SECTION_NAME": "Alle", "MOVE_TO_COLLECTION": "Zum Album verschieben", - "UNARCHIVE": "", - "UNARCHIVE_COLLECTION": "", - "HIDE_COLLECTION": "", - "UNHIDE_COLLECTION": "", + "UNARCHIVE": "Dearchivieren", + "UNARCHIVE_COLLECTION": "Album dearchivieren", + "HIDE_COLLECTION": "Album ausblenden", + "UNHIDE_COLLECTION": "Album wieder einblenden", "MOVE": "Verschieben", "ADD": "Hinzufügen", "REMOVE": "Entfernen", @@ -336,86 +334,86 @@ "LEAVE_SHARED_ALBUM_MESSAGE": "", "NOT_FILE_OWNER": "Dateien in einem freigegebenen Album können nicht gelöscht werden", "CONFIRM_SELF_REMOVE_MESSAGE": "", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", "SORT_BY_CREATION_TIME_ASCENDING": "Ältestem", "SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert", "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "", - "THUMBNAIL_REPLACED": "", + "COMPRESS_THUMBNAILS": "Vorschaubilder komprimieren", + "THUMBNAIL_REPLACED": "Vorschaubilder komprimiert", "FIX_THUMBNAIL": "Komprimiere", - "FIX_THUMBNAIL_LATER": "", + "FIX_THUMBNAIL_LATER": "Später komprimieren", "REPLACE_THUMBNAIL_NOT_STARTED": "", "REPLACE_THUMBNAIL_COMPLETED": "", "REPLACE_THUMBNAIL_NOOP": "", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", - "FIX_CREATION_TIME": "", - "FIX_CREATION_TIME_IN_PROGRESS": "", - "CREATION_TIME_UPDATED": "", - "UPDATE_CREATION_TIME_NOT_STARTED": "", + "FIX_CREATION_TIME": "Zeit reparieren", + "FIX_CREATION_TIME_IN_PROGRESS": "Zeit wird repariert", + "CREATION_TIME_UPDATED": "Datei-Zeit aktualisiert", + "UPDATE_CREATION_TIME_NOT_STARTED": "Wählen Sie die Option, die Sie verwenden möchten", "UPDATE_CREATION_TIME_COMPLETED": "", "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", - "CAPTION_CHARACTER_LIMIT": "", + "CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen", "DATE_TIME_ORIGINAL": "", "DATE_TIME_DIGITIZED": "", "METADATA_DATE": "", - "CUSTOM_TIME": "", + "CUSTOM_TIME": "Benutzerdefinierte Zeit", "REOPEN_PLAN_SELECTOR_MODAL": "", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Fehler beim Öffnen der Pläne", "INSTALL": "Installieren", "SHARING_DETAILS": "Details teilen", - "MODIFY_SHARING": "", - "ADD_COLLABORATORS": "", - "ADD_NEW_EMAIL": "", - "shared_with_people_zero": "", - "shared_with_people_one": "", - "shared_with_people_other": "", - "participants_zero": "", - "participants_one": "", - "participants_other": "", - "ADD_VIEWERS": "", - "PARTICIPANTS": "", + "MODIFY_SHARING": "Freigabe ändern", + "ADD_COLLABORATORS": "Bearbeiter hinzufügen", + "ADD_NEW_EMAIL": "Neue E-Mail-Adresse hinzufügen", + "shared_with_people_zero": "Mit bestimmten Personen teilen", + "shared_with_people_one": "Geteilt mit einer Person", + "shared_with_people_other": "Geteilt mit {{count, number}} Personen", + "participants_zero": "Keine Teilnehmer", + "participants_one": "1 Teilnehmer", + "participants_other": "{{count, number}} Teilnehmer", + "ADD_VIEWERS": "Betrachter hinzufügen", + "PARTICIPANTS": "Teilnehmer", "CHANGE_PERMISSIONS_TO_VIEWER": "", "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", - "CONVERT_TO_VIEWER": "", + "CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern", "CONVERT_TO_COLLABORATOR": "", - "CHANGE_PERMISSION": "", + "CHANGE_PERMISSION": "Berechtigung ändern?", "REMOVE_PARTICIPANT": "Entfernen?", "CONFIRM_REMOVE": "Ja, entfernen", "MANAGE": "Verwalten", - "ADDED_AS": "", - "COLLABORATOR_RIGHTS": "", + "ADDED_AS": "Hinzugefügt als", + "COLLABORATOR_RIGHTS": "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen", "REMOVE_PARTICIPANT_HEAD": "Teilnehmer entfernen", "OWNER": "Besitzer", - "COLLABORATORS": "", - "ADD_MORE": "", - "VIEWERS": "", - "OR_ADD_EXISTING": "", + "COLLABORATORS": "Bearbeiter", + "ADD_MORE": "Mehr hinzufügen", + "VIEWERS": "Zuschauer", + "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - Nicht gefunden", "LINK_EXPIRED": "Link ist abgelaufen", "LINK_EXPIRED_MESSAGE": "Dieser Link ist abgelaufen oder wurde deaktiviert!", - "MANAGE_LINK": "", + "MANAGE_LINK": "Link verwalten", "LINK_TOO_MANY_REQUESTS": "Sorry, dieses Album wurde auf zu vielen Geräten angezeigt!", "FILE_DOWNLOAD": "Downloads erlauben", "LINK_PASSWORD_LOCK": "Passwort Sperre", - "PUBLIC_COLLECT": "", + "PUBLIC_COLLECT": "Hinzufügen von Fotos erlauben", "LINK_DEVICE_LIMIT": "Geräte Limit", - "NO_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "Keins", "LINK_EXPIRY": "Ablaufdatum des Links", "NEVER": "Niemals", - "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD": "Download deaktivieren", "DISABLE_FILE_DOWNLOAD_MESSAGE": "", - "MALICIOUS_CONTENT": "", - "COPYRIGHT": "", - "SHARED_USING": "", + "MALICIOUS_CONTENT": "Enthält schädliche Inhalte", + "COPYRIGHT": "Verletzung des Urheberrechts von jemandem, den ich repräsentieren darf", + "SHARED_USING": "Freigegeben über ", "ENTE_IO": "ente.io", "SHARING_REFERRAL_CODE": "", "LIVE": "LIVE", - "DISABLE_PASSWORD": "", - "DISABLE_PASSWORD_MESSAGE": "", + "DISABLE_PASSWORD": "Passwort-Sperre deaktivieren", + "DISABLE_PASSWORD_MESSAGE": "Sind Sie sicher, dass Sie die Passwort-Sperre deaktivieren möchten?", "PASSWORD_LOCK": "Passwort Sperre", - "LOCK": "", - "DOWNLOAD_UPLOAD_LOGS": "", + "LOCK": "Sperren", + "DOWNLOAD_UPLOAD_LOGS": "Debug-Logs", "UPLOAD_FILES": "Datei", "UPLOAD_DIRS": "Ordner", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", @@ -469,13 +467,13 @@ "FAMILY_PLAN": "Familientarif", "DOWNLOAD_LOGS": "Logs herunterladen", "DOWNLOAD_LOGS_MESSAGE": "", - "CHANGE_FOLDER": "", - "TWO_MONTHS_FREE": "", + "CHANGE_FOLDER": "Ordner ändern", + "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", "GB": "GB", "POPULAR": "Beliebt", - "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren", "FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr", - "CURRENT_USAGE": "", + "CURRENT_USAGE": "Aktuelle Nutzung ist {{usage}}", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/en-US/translation.json b/web/apps/accounts/public/locales/en-US/translation.json index 6870df319..b06336bf5 100644 --- a/web/apps/accounts/public/locales/en-US/translation.json +++ b/web/apps/accounts/public/locales/en-US/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Login", "SIGN_UP": "Signup", - "NEW_USER": "New to ente", + "NEW_USER": "New to Ente", "EXISTING_USER": "Existing user", "ENTER_NAME": "Enter name", "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", "CREATE_COLLECTION": "New album", "ENTER_ALBUM_NAME": "Album name", "CLOSE_OPTION": "Close (Esc)", @@ -95,7 +93,7 @@ "TRASH_FILES_TITLE": "Delete files?", "TRASH_FILE_TITLE": "Delete file?", "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", + "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your Ente account.", "DELETE": "Delete", "DELETE_OPTION": "Delete (DEL)", "FAVORITE_OPTION": "Favorite (L)", @@ -107,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", + "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets Ente's encryption standards, please try using the mobile app or another browser", "CHANGE_PASSWORD": "Change password", "GO_BACK": "Go back", "RECOVERY_KEY": "Recovery key", @@ -280,11 +278,11 @@ "LAST_EXPORT_TIME": "Last export time", "EXPORT_AGAIN": "Resync", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking Ente from saving data into local storage. please try loading this page after switching your browsing mode.", "SEND_OTT": "Send OTP", "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", + "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing Ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", + "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for Ente and share with the intended recipients using their email.

", "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", "RETRY_FAILED": "Retry failed uploads", "FAILED_UPLOADS": "Failed uploads ", @@ -293,7 +291,7 @@ "UNSUPPORTED_FILES": "Unsupported files", "SUCCESSFUL_UPLOADS": "Successful uploads", "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", + "UNSUPPORTED_INFO": "Ente does not support these file formats yet", "BLOCKED_UPLOADS": "Blocked uploads", "SKIPPED_VIDEOS": "Skipped videos", "INPROGRESS_METADATA_EXTRACTION": "In progress", @@ -329,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restore to album", "EMPTY_TRASH": "Empty trash", "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", + "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your Ente account.", "LEAVE_SHARED_ALBUM": "Yes, leave", "LEAVE_ALBUM": "Leave album", "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", @@ -344,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails compressed", "FIX_THUMBNAIL": "Compress", "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like Ente to compress them?", "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", @@ -423,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", + "FILES": "files", + "EACH": "each", "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", "STOP_UPLOADS_HEADER": "Stop uploads?", @@ -457,12 +455,12 @@ "WATCHED_FOLDERS": "Watched folders", "NO_FOLDERS_ADDED": "No folders added yet!", "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", + "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from Ente", "ADD_FOLDER": "Add folder", "STOP_WATCHING": "Stop watching", "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", + "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but Ente will stop automatically updating the linked Ente album on changes in this folder.", "YES_STOP": "Yes, stop", "MONTH_SHORT": "mo", "YEAR": "year", @@ -476,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Continue with free trial", "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", + "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to Ente on your computer, or download the Ente mobile/desktop app.", + "DRAG_AND_DROP_HINT": "Or drag and drop into the Ente window", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", "AUTHENTICATE": "Authenticate", "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", "NEVERMIND": "Nevermind", "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", + "UPDATE_INSTALLABLE_MESSAGE": "A new version of Ente is ready to be installed.", "INSTALL_NOW": "Install now", "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", + "UPDATE_AVAILABLE_MESSAGE": "A new version of Ente has been released, but it cannot be automatically downloaded and installed.", "DOWNLOAD_AND_INSTALL": "Download and install", "IGNORE_THIS_VERSION": "Ignore this version", "TODAY": "Today", @@ -501,13 +499,13 @@ "ML_MORE_DETAILS": "More details", "ENABLE_FACE_SEARCH": "Enable face recognition", "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", "DISABLE_BETA": "Pause recognition", "DISABLE_FACE_SEARCH": "Disable face recognition", "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", + "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow Ente to process face geometry", "LABS": "Labs", "YOURS": "yours", "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Downloading {{name}}", "DOWNLOAD_FAILED": "Download failed", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", "CHRISTMAS": "Christmas", "CHRISTMAS_EVE": "Christmas Eve", "NEW_YEAR": "New Year", @@ -600,7 +597,7 @@ "LIVE_PHOTO": "Live Photo", "CONVERT": "Convert", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to Ente to persist your changes.", "BRIGHTNESS": "Brightness", "CONTRAST": "Contrast", "SATURATION": "Saturation", @@ -613,7 +610,7 @@ "FLIP_VERTICALLY": "Flip Vertically", "FLIP_HORIZONTALLY": "Flip Horizontally", "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", + "SAVE_A_COPY_TO_ENTE": "Save a copy to Ente", "RESTORE_ORIGINAL": "Restore Original", "TRANSFORM": "Transform", "COLORS": "Colors", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", "FREEHAND": "Freehand", "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.", + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Delete passkey", + "DELETE_PASSKEY_CONFIRMATION": "Are you sure you want to delete this passkey? This action is irreversible.", + "RENAME_PASSKEY": "Rename passkey", + "ADD_PASSKEY": "Add passkey", + "ENTER_PASSKEY_NAME": "Enter passkey name", + "PASSKEYS_DESCRIPTION": "Passkeys are a modern and secure second-factor for your Ente account. They use on-device biometric authentication for convenience and security.", + "CREATED_AT": "Created at", + "PASSKEY_LOGIN_FAILED": "Passkey login failed", + "PASSKEY_LOGIN_URL_INVALID": "The login URL is invalid.", + "PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.", + "TRY_AGAIN": "Try again", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.", + "LOGIN_WITH_PASSKEY": "Login with passkey" } diff --git a/web/apps/accounts/public/locales/es-ES/translation.json b/web/apps/accounts/public/locales/es-ES/translation.json index 4adb22ee5..a29165e4e 100644 --- a/web/apps/accounts/public/locales/es-ES/translation.json +++ b/web/apps/accounts/public/locales/es-ES/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Esta es una característica del navegador destinada a los desarrolladores. Por favor, no copie y pegue código sin verificar aquí.", "CREATE_COLLECTION": "Nuevo álbum", "ENTER_ALBUM_NAME": "Nombre del álbum", "CLOSE_OPTION": "Cerrar (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/fa-IR/translation.json b/web/apps/accounts/public/locales/fa-IR/translation.json index 3817bccd8..2d21fcb3d 100644 --- a/web/apps/accounts/public/locales/fa-IR/translation.json +++ b/web/apps/accounts/public/locales/fa-IR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/fi-FI/translation.json b/web/apps/accounts/public/locales/fi-FI/translation.json index bc335bc77..888ed7093 100644 --- a/web/apps/accounts/public/locales/fi-FI/translation.json +++ b/web/apps/accounts/public/locales/fi-FI/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/fr-FR/translation.json b/web/apps/accounts/public/locales/fr-FR/translation.json index 0d878a703..43d959069 100644 --- a/web/apps/accounts/public/locales/fr-FR/translation.json +++ b/web/apps/accounts/public/locales/fr-FR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Comment avez-vous entendu parler de Ente? (facultatif)", "REFERRAL_INFO": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", "PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Ceci est une fonction de navigateur dédiée aux développeurs. Veuillez ne pas copier-coller un code non vérifié à cet endroit.", "CREATE_COLLECTION": "Nouvel album", "ENTER_ALBUM_NAME": "Nom de l'album", "CLOSE_OPTION": "Fermer (Échap)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Téléchargement de {{name}}", "DOWNLOAD_FAILED": "Échec du téléchargement", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} fichiers", - "CRASH_REPORTING": "Rapport de plantage", "CHRISTMAS": "Noël", "CHRISTMAS_EVE": "Réveillon de Noël", "NEW_YEAR": "Nouvel an", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Visitez cast.ente.io sur l'appareil que vous voulez associer.", "CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.", "CACHE_DIRECTORY": "Dossier du cache", - "PASSKEYS": "Clés d'accès", "FREEHAND": "Main levée", "APPLY_CROP": "Appliquer le recadrage", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.", + "PASSKEYS": "Clés d'accès", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/it-IT/translation.json b/web/apps/accounts/public/locales/it-IT/translation.json index 3d8dcb3c6..ae450e5fe 100644 --- a/web/apps/accounts/public/locales/it-IT/translation.json +++ b/web/apps/accounts/public/locales/it-IT/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Come hai conosciuto Ente? (opzionale)", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Le password non corrispondono", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Questa è una funzionalità del browser destinata agli sviluppatori. Non copiare né incollare codice non verificato qui.", "CREATE_COLLECTION": "Nuovo album", "ENTER_ALBUM_NAME": "Nome album", "CLOSE_OPTION": "Chiudi (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/ko-KR/translation.json b/web/apps/accounts/public/locales/ko-KR/translation.json index bc335bc77..4fbe6c077 100644 --- a/web/apps/accounts/public/locales/ko-KR/translation.json +++ b/web/apps/accounts/public/locales/ko-KR/translation.json @@ -1,83 +1,81 @@ { - "HERO_SLIDE_1_TITLE": "", - "HERO_SLIDE_1": "", - "HERO_SLIDE_2_TITLE": "", - "HERO_SLIDE_2": "", - "HERO_SLIDE_3_TITLE": "", - "HERO_SLIDE_3": "", - "LOGIN": "", - "SIGN_UP": "", - "NEW_USER": "", - "EXISTING_USER": "", - "ENTER_NAME": "", - "PUBLIC_UPLOADER_NAME_MESSAGE": "", - "ENTER_EMAIL": "", - "EMAIL_ERROR": "", - "REQUIRED": "", - "EMAIL_SENT": "", - "CHECK_INBOX": "", - "ENTER_OTT": "", - "RESEND_MAIL": "", - "VERIFY": "", - "UNKNOWN_ERROR": "", - "INVALID_CODE": "", - "EXPIRED_CODE": "", - "SENDING": "", - "SENT": "", - "PASSWORD": "", - "LINK_PASSWORD": "", - "RETURN_PASSPHRASE_HINT": "", - "SET_PASSPHRASE": "", - "VERIFY_PASSPHRASE": "", - "INCORRECT_PASSPHRASE": "", - "ENTER_ENC_PASSPHRASE": "", - "PASSPHRASE_DISCLAIMER": "", - "WELCOME_TO_ENTE_HEADING": "", - "WELCOME_TO_ENTE_SUBHEADING": "", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", - "PASSPHRASE_HINT": "", - "CONFIRM_PASSPHRASE": "", - "REFERRAL_CODE_HINT": "", - "REFERRAL_INFO": "", - "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", - "CREATE_COLLECTION": "", - "ENTER_ALBUM_NAME": "", - "CLOSE_OPTION": "", - "ENTER_FILE_NAME": "", - "CLOSE": "", - "NO": "", - "NOTHING_HERE": "", - "UPLOAD": "", - "IMPORT": "", - "ADD_PHOTOS": "", - "ADD_MORE_PHOTOS": "", - "add_photos_one": "", - "add_photos_other": "", - "SELECT_PHOTOS": "", - "FILE_UPLOAD": "", + "HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요", + "HERO_SLIDE_1": "종단간 암호화가 기본지원입니다", + "HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다", + "HERO_SLIDE_2": "오랫동안 보존할 수 있도록한 설계", + "HERO_SLIDE_3_TITLE": "
어디에서나
이용가능
", + "HERO_SLIDE_3": "안드로이드, iOS, 웹, 데스크탑", + "LOGIN": "로그인", + "SIGN_UP": "회원가입", + "NEW_USER": "ente의 새소식", + "EXISTING_USER": "기존 사용자", + "ENTER_NAME": "이름 입력", + "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", + "ENTER_EMAIL": "이메일 주소를 입력하세요", + "EMAIL_ERROR": "올바른 이메일을 입력하세요", + "REQUIRED": "필수", + "EMAIL_SENT": "{{email}} 로 인증 코드가 전송되었습니다", + "CHECK_INBOX": "인증을 완료하기 위해 당신의 메일 수신함(그리고 스팸 수신함)을 확인하세요.", + "ENTER_OTT": "인증 코드", + "RESEND_MAIL": "코드 재전송하기", + "VERIFY": "인증", + "UNKNOWN_ERROR": "문제가 생긴 것 같아요. 다시 시도하세요", + "INVALID_CODE": "잘못된 인증 코드", + "EXPIRED_CODE": "입력한 인증 코드가 만료되었습니다", + "SENDING": "전송 중...", + "SENT": "발송 완료!", + "PASSWORD": "비밀번호", + "LINK_PASSWORD": "앨범 잠금해제를 위해 비밀번호를 입력하세요", + "RETURN_PASSPHRASE_HINT": "비밀번호", + "SET_PASSPHRASE": "비밀번호 설정", + "VERIFY_PASSPHRASE": "로그인", + "INCORRECT_PASSPHRASE": "잘못된 비밀번호입니다", + "ENTER_ENC_PASSPHRASE": "당신의 데이터를 암호화하는 데 사용할 수 있는 비밀번호를 입력하세요", + "PASSPHRASE_DISCLAIMER": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키 없다면 데이터 복구를 도와드릴 수 없습니다.", + "WELCOME_TO_ENTE_HEADING": "환영합니다 ", + "WELCOME_TO_ENTE_SUBHEADING": "End-to-End 암호화된 사진 저장 및 공유", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "당신 최고의 사진이 있는 곳", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "암호 키 생성 중...", + "PASSPHRASE_HINT": "비밀번호", + "CONFIRM_PASSPHRASE": "비밀번호 확인", + "REFERRAL_CODE_HINT": "어떻게 Ente에 대해 들으셨나요? (선택사항)", + "REFERRAL_INFO": "우리는 앱 설치를 추적하지 않습니다. 우리를 알게 된 곳을 남겨주시면 우리에게 도움이 될꺼에요!", + "PASSPHRASE_MATCH_ERROR": "비밀번호가 일치하지 않습니다", + "CREATE_COLLECTION": "새 앨범", + "ENTER_ALBUM_NAME": "앨범 이름", + "CLOSE_OPTION": "닫기 (Esc)", + "ENTER_FILE_NAME": "파일 이름", + "CLOSE": "닫기", + "NO": "아니오", + "NOTHING_HERE": "아직 볼 수 있는 것이 없어요 👀", + "UPLOAD": "업로드", + "IMPORT": "가져오기", + "ADD_PHOTOS": "사진 추가", + "ADD_MORE_PHOTOS": "사진 더 추가하기", + "add_photos_one": "아이템 하나 추가", + "add_photos_other": "아이템 {{count, number}} 개 추가하기", + "SELECT_PHOTOS": "사진 선택하기", + "FILE_UPLOAD": "파일 업로드", "UPLOAD_STAGE_MESSAGE": { - "0": "", - "1": "", - "2": "", - "3": "", - "4": "", - "5": "" + "0": "업로드 준비중", + "1": "구글 메타데이타 파일들 읽는중", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일 메타데이터가 추출되었습니다", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일이 처리되었습니다", + "4": "남은 업로드 취소중", + "5": "백업 완료" }, - "FILE_NOT_UPLOADED_LIST": "", - "SUBSCRIPTION_EXPIRED": "", - "SUBSCRIPTION_EXPIRED_MESSAGE": "", - "STORAGE_QUOTA_EXCEEDED": "", - "INITIAL_LOAD_DELAY_WARNING": "", - "USER_DOES_NOT_EXIST": "", - "NO_ACCOUNT": "", - "ACCOUNT_EXISTS": "", - "CREATE": "", - "DOWNLOAD": "", - "DOWNLOAD_OPTION": "", - "DOWNLOAD_FAVORITES": "", + "FILE_NOT_UPLOADED_LIST": "아래 파일들은 업로드 되지 않았습니다", + "SUBSCRIPTION_EXPIRED": "구독 만료", + "SUBSCRIPTION_EXPIRED_MESSAGE": "당신 구독이 만료되었으니, 구독을 갱신해주세요", + "STORAGE_QUOTA_EXCEEDED": "스토리지 제한이 초과되었습니다", + "INITIAL_LOAD_DELAY_WARNING": "처음 로딩시 다소 시간이 걸릴 수 있습니다", + "USER_DOES_NOT_EXIST": "죄송합니다. 해당 이메일을 사용하는 사용자를 찾을 수 없습니다", + "NO_ACCOUNT": "계정이 없습니다", + "ACCOUNT_EXISTS": "이미 계정이 있습니다", + "CREATE": "만들기", + "DOWNLOAD": "다운로드", + "DOWNLOAD_OPTION": "다운로드 (D)", + "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", "DOWNLOAD_UNCATEGORIZED": "", "DOWNLOAD_HIDDEN_ITEMS": "", "COPY_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/nl-NL/translation.json b/web/apps/accounts/public/locales/nl-NL/translation.json index df869b0dd..15d9bfdba 100644 --- a/web/apps/accounts/public/locales/nl-NL/translation.json +++ b/web/apps/accounts/public/locales/nl-NL/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)", "REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", "PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Dit is een browserfunctie bedoeld voor ontwikkelaars. Gelieve hier geen niet-geverifieerde code te kopiëren/plakken.", "CREATE_COLLECTION": "Nieuw album", "ENTER_ALBUM_NAME": "Album naam", "CLOSE_OPTION": "Sluiten (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "{{name}} downloaden", "DOWNLOAD_FAILED": "Download mislukt", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} bestanden", - "CRASH_REPORTING": "Foutenrapportering", "CHRISTMAS": "Kerst", "CHRISTMAS_EVE": "Kerstavond", "NEW_YEAR": "Nieuwjaar", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "Cache map", - "PASSKEYS": "", "FREEHAND": "Losse hand", "APPLY_CROP": "Bijsnijden toepassen", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/pt-BR/translation.json b/web/apps/accounts/public/locales/pt-BR/translation.json index 5145a24aa..0da001742 100644 --- a/web/apps/accounts/public/locales/pt-BR/translation.json +++ b/web/apps/accounts/public/locales/pt-BR/translation.json @@ -8,7 +8,7 @@ "LOGIN": "Entrar", "SIGN_UP": "Registrar", "NEW_USER": "Novo no ente", - "EXISTING_USER": "Utilizador existente", + "EXISTING_USER": "Usuário existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", "ENTER_EMAIL": "Insira o endereço de e-mail", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Como você ouviu sobre o Ente? (opcional)", "REFERRAL_INFO": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "PASSPHRASE_MATCH_ERROR": "As senhas não coincidem", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "Este é um recurso de navegador destinado a desenvolvedores. Por favor, não copie e cole o código não confirmado aqui.", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", @@ -229,7 +227,7 @@ "INDEXING_SCHEDULED": "Indexação está programada...", "ANALYZING_PHOTOS": "Indexando fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", "INDEXING_PEOPLE": "Indexando pessoas em {{indexStatus.nSyncedFiles,number}} fotos...", - "INDEXING_DONE": "", + "INDEXING_DONE": "Foram indexadas {{indexStatus.nSyncedFiles,number}} fotos", "UNIDENTIFIED_FACES": "rostos não identificados", "OBJECTS": "objetos", "TEXT": "texto", @@ -349,15 +347,15 @@ "REPLACE_THUMBNAIL_NOOP": "Você não tem nenhuma miniatura que possa ser compactadas mais", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Não foi possível compactar algumas das suas miniaturas, por favor tente novamente", "FIX_CREATION_TIME": "Corrigir hora", - "FIX_CREATION_TIME_IN_PROGRESS": "", - "CREATION_TIME_UPDATED": "", + "FIX_CREATION_TIME_IN_PROGRESS": "Corrigindo horário", + "CREATION_TIME_UPDATED": "Hora do arquivo atualizado", "UPDATE_CREATION_TIME_NOT_STARTED": "Selecione a carteira que você deseja usar", "UPDATE_CREATION_TIME_COMPLETED": "Todos os arquivos atualizados com sucesso", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "A atualização do horário falhou para alguns arquivos, por favor, tente novamente", "CAPTION_CHARACTER_LIMIT": "5000 caracteres no máximo", - "DATE_TIME_ORIGINAL": "", - "DATE_TIME_DIGITIZED": "", - "METADATA_DATE": "", + "DATE_TIME_ORIGINAL": "Data e Hora Original", + "DATE_TIME_DIGITIZED": "Data e Hora Digitalizada", + "METADATA_DATE": "Data de Metadados", "CUSTOM_TIME": "Tempo personalizado", "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planos", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Falha ao abrir planos", @@ -410,7 +408,7 @@ "SHARED_USING": "Compartilhar usando ", "ENTE_IO": "ente.io", "SHARING_REFERRAL_CODE": "Use o código {{referralCode}} para obter 10 GB de graça", - "LIVE": "", + "LIVE": "AO VIVO", "DISABLE_PASSWORD": "Desativar bloqueio por senha", "DISABLE_PASSWORD_MESSAGE": "Tem certeza que deseja desativar o bloqueio por senha?", "PASSWORD_LOCK": "Bloqueio de senha", @@ -508,8 +506,8 @@ "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente irá parar de processar geometria facial.

Você pode reativar o reconhecimento facial novamente, se desejar, então esta operação está segura.

", "ADVANCED": "Avançado", "FACE_SEARCH_CONFIRMATION": "Eu entendo, e desejo permitir que o ente processe a geometria do rosto", - "LABS": "", - "YOURS": "", + "LABS": "Laboratórios", + "YOURS": "seu", "PASSPHRASE_STRENGTH_WEAK": "Força da senha: fraca", "PASSPHRASE_STRENGTH_MODERATE": "Força da senha: moderada", "PASSPHRASE_STRENGTH_STRONG": "Força da senha: forte", @@ -572,7 +570,7 @@ "FEEDBACK_REQUIRED": "Por favor, ajude-nos com esta informação", "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "O que o outro serviço faz melhor?", "RECOVER_TWO_FACTOR": "Recuperar dois fatores", - "at": "", + "at": "em", "AUTH_NEXT": "próximo", "AUTH_DOWNLOAD_MOBILE_APP": "Baixe nosso aplicativo móvel para gerenciar seus segredos", "HIDDEN": "Escondido", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Transferindo {{name}}", "DOWNLOAD_FAILED": "Falha ao baixar", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", - "CRASH_REPORTING": "Relatório de falhas", "CHRISTMAS": "Natal", "CHRISTMAS_EVE": "Véspera de Natal", "NEW_YEAR": "Ano Novo", @@ -607,7 +604,7 @@ "BLUR": "Desfoque", "INVERT_COLORS": "Inverter Cores", "ASPECT_RATIO": "Proporção da imagem", - "SQUARE": "", + "SQUARE": "Quadrado", "ROTATE_LEFT": "Girar para a Esquerda", "ROTATE_RIGHT": "Girar para a Direita", "FLIP_VERTICALLY": "Inverter verticalmente", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Acesse cast.ente.io no dispositivo que você deseja parear.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.", "CACHE_DIRECTORY": "Pasta de Cache", - "PASSKEYS": "", - "FREEHAND": "", + "FREEHAND": "Mão livre", "APPLY_CROP": "Aplicar Recorte", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", + "PASSKEYS": "Chaves de acesso", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/pt-PT/translation.json b/web/apps/accounts/public/locales/pt-PT/translation.json index fb33bb972..230980326 100644 --- a/web/apps/accounts/public/locales/pt-PT/translation.json +++ b/web/apps/accounts/public/locales/pt-PT/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/ru-RU/translation.json b/web/apps/accounts/public/locales/ru-RU/translation.json index d551758ad..c85db2236 100644 --- a/web/apps/accounts/public/locales/ru-RU/translation.json +++ b/web/apps/accounts/public/locales/ru-RU/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Как вы узнали о Ente? (необязательно)", "REFERRAL_INFO": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения!", "PASSPHRASE_MATCH_ERROR": "Пароли не совпадают", - "CONSOLE_WARNING_STOP": "Остановись!", - "CONSOLE_WARNING_DESC": "Это функция браузера, предназначенная для разработчиков. Пожалуйста, не копируйте и не вставляйте сюда непроверенный код.", "CREATE_COLLECTION": "Новый альбом", "ENTER_ALBUM_NAME": "Название альбома", "CLOSE_OPTION": "Закрыть (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Загрузка {{name}}", "DOWNLOAD_FAILED": "Загрузка не удалась", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} файлов", - "CRASH_REPORTING": "Отчеты об ошибках", "CHRISTMAS": "Рождество", "CHRISTMAS_EVE": "Канун Рождества", "NEW_YEAR": "Новый год", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/sv-SE/translation.json b/web/apps/accounts/public/locales/sv-SE/translation.json index bc335bc77..f88535795 100644 --- a/web/apps/accounts/public/locales/sv-SE/translation.json +++ b/web/apps/accounts/public/locales/sv-SE/translation.json @@ -9,9 +9,9 @@ "SIGN_UP": "", "NEW_USER": "", "EXISTING_USER": "", - "ENTER_NAME": "", + "ENTER_NAME": "Ange namn", "PUBLIC_UPLOADER_NAME_MESSAGE": "", - "ENTER_EMAIL": "", + "ENTER_EMAIL": "Ange e-postadress", "EMAIL_ERROR": "", "REQUIRED": "", "EMAIL_SENT": "", @@ -24,31 +24,29 @@ "EXPIRED_CODE": "", "SENDING": "", "SENT": "", - "PASSWORD": "", + "PASSWORD": "Lösenord", "LINK_PASSWORD": "", - "RETURN_PASSPHRASE_HINT": "", + "RETURN_PASSPHRASE_HINT": "Lösenord", "SET_PASSPHRASE": "", - "VERIFY_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "Logga in", "INCORRECT_PASSPHRASE": "", "ENTER_ENC_PASSPHRASE": "", "PASSPHRASE_DISCLAIMER": "", - "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_HEADING": "Välkommen till ", "WELCOME_TO_ENTE_SUBHEADING": "", "WHERE_YOUR_BEST_PHOTOS_LIVE": "", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", - "PASSPHRASE_HINT": "", - "CONFIRM_PASSPHRASE": "", + "PASSPHRASE_HINT": "Lösenord", + "CONFIRM_PASSPHRASE": "Bekräfta lösenord", "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", - "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", + "PASSPHRASE_MATCH_ERROR": "Lösenorden matchar inte", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", - "ENTER_FILE_NAME": "", - "CLOSE": "", - "NO": "", + "ENTER_FILE_NAME": "Filnamn", + "CLOSE": "Stäng", + "NO": "Nej", "NOTHING_HERE": "", "UPLOAD": "", "IMPORT": "", @@ -96,31 +94,31 @@ "TRASH_FILE_TITLE": "", "DELETE_FILES_TITLE": "", "DELETE_FILES_MESSAGE": "", - "DELETE": "", + "DELETE": "Radera", "DELETE_OPTION": "", "FAVORITE_OPTION": "", "UNFAVORITE_OPTION": "", "MULTI_FOLDER_UPLOAD": "", "UPLOAD_STRATEGY_CHOICE": "", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", - "OR": "", + "OR": "eller", "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", "SESSION_EXPIRED_MESSAGE": "", "SESSION_EXPIRED": "", "PASSWORD_GENERATION_FAILED": "", - "CHANGE_PASSWORD": "", + "CHANGE_PASSWORD": "Ändra lösenord", "GO_BACK": "", - "RECOVERY_KEY": "", + "RECOVERY_KEY": "Återställningsnyckel", "SAVE_LATER": "", - "SAVE": "", + "SAVE": "Spara nyckel", "RECOVERY_KEY_DESCRIPTION": "", "RECOVER_KEY_GENERATION_FAILED": "", "KEY_NOT_STORED_DISCLAIMER": "", - "FORGOT_PASSWORD": "", + "FORGOT_PASSWORD": "Glömt lösenord", "RECOVER_ACCOUNT": "", "RECOVERY_KEY_HINT": "", "RECOVER": "", - "NO_RECOVERY_KEY": "", + "NO_RECOVERY_KEY": "Ingen återställningsnyckel?", "INCORRECT_RECOVERY_KEY": "", "SORRY": "", "NO_RECOVERY_KEY_MESSAGE": "", @@ -128,30 +126,30 @@ "CONTACT_SUPPORT": "", "REQUEST_FEATURE": "", "SUPPORT": "", - "CONFIRM": "", - "CANCEL": "", - "LOGOUT": "", - "DELETE_ACCOUNT": "", + "CONFIRM": "Bekräfta", + "CANCEL": "Avbryt", + "LOGOUT": "Logga ut", + "DELETE_ACCOUNT": "Radera konto", "DELETE_ACCOUNT_MESSAGE": "", "LOGOUT_MESSAGE": "", "CHANGE_EMAIL": "", "OK": "", "SUCCESS": "", "ERROR": "", - "MESSAGE": "", + "MESSAGE": "Meddelande", "INSTALL_MOBILE_APP": "", "DOWNLOAD_APP_MESSAGE": "", "DOWNLOAD_APP": "", "EXPORT": "", "SUBSCRIPTION": "", - "SUBSCRIBE": "", - "MANAGEMENT_PORTAL": "", + "SUBSCRIBE": "Prenumerera", + "MANAGEMENT_PORTAL": "Hantera betalningsmetod", "MANAGE_FAMILY_PORTAL": "", "LEAVE_FAMILY_PLAN": "", "LEAVE": "", "LEAVE_FAMILY_CONFIRM": "", "CHOOSE_PLAN": "", - "MANAGE_PLAN": "", + "MANAGE_PLAN": "Hantera din prenumeration", "ACTIVE": "", "OFFLINE_MSG": "", "FREE_SUBSCRIPTION_INFO": "", @@ -203,18 +201,18 @@ "DOWNLOAD_COLLECTION": "", "DOWNLOAD_COLLECTION_MESSAGE": "", "CREATE_ALBUM_FAILED": "", - "SEARCH": "", - "SEARCH_RESULTS": "", - "NO_RESULTS": "", + "SEARCH": "Sök", + "SEARCH_RESULTS": "Sökresultat", + "NO_RESULTS": "Inga resultat hittades", "SEARCH_HINT": "", "SEARCH_TYPE": { "COLLECTION": "", "LOCATION": "", "CITY": "", - "DATE": "", + "DATE": "Datum", "FILE_NAME": "", "THING": "", - "FILE_CAPTION": "", + "FILE_CAPTION": "Beskrivning", "FILE_TYPE": "", "CLIP": "" }, @@ -369,11 +367,11 @@ "shared_with_people_zero": "", "shared_with_people_one": "", "shared_with_people_other": "", - "participants_zero": "", - "participants_one": "", + "participants_zero": "Inga deltagare", + "participants_one": "1 deltagare", "participants_other": "", "ADD_VIEWERS": "", - "PARTICIPANTS": "", + "PARTICIPANTS": "Deltagare", "CHANGE_PERMISSIONS_TO_VIEWER": "", "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", "CONVERT_TO_VIEWER": "", @@ -416,14 +414,14 @@ "PASSWORD_LOCK": "", "LOCK": "", "DOWNLOAD_UPLOAD_LOGS": "", - "UPLOAD_FILES": "", - "UPLOAD_DIRS": "", + "UPLOAD_FILES": "Fil", + "UPLOAD_DIRS": "Mapp", "UPLOAD_GOOGLE_TAKEOUT": "", "DEDUPLICATE_FILES": "", "AUTHENTICATOR_SECTION": "", "NO_DUPLICATES_FOUND": "", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "", + "FILES": "Filer", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", @@ -432,8 +430,8 @@ "STOP_DOWNLOADS_HEADER": "", "YES_STOP_DOWNLOADS": "", "STOP_ALL_DOWNLOADS_MESSAGE": "", - "albums_one": "", - "albums_other": "", + "albums_one": "1 album", + "albums_other": "{{count, number}} album", "ALL_ALBUMS": "", "ALBUMS": "", "ALL_HIDDEN_ALBUMS": "", @@ -459,19 +457,19 @@ "FOLDERS_AUTOMATICALLY_MONITORED": "", "UPLOAD_NEW_FILES_TO_ENTE": "", "REMOVE_DELETED_FILES_FROM_ENTE": "", - "ADD_FOLDER": "", + "ADD_FOLDER": "Lägg till mapp", "STOP_WATCHING": "", "STOP_WATCHING_FOLDER": "", "STOP_WATCHING_DIALOG_MESSAGE": "", "YES_STOP": "", - "MONTH_SHORT": "", - "YEAR": "", + "MONTH_SHORT": "mån", + "YEAR": "år", "FAMILY_PLAN": "", "DOWNLOAD_LOGS": "", "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", + "GB": "GB", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", "FREE_PLAN_DESCRIPTION": "", @@ -492,7 +490,7 @@ "IGNORE_THIS_VERSION": "", "TODAY": "", "YESTERDAY": "", - "NAME_PLACEHOLDER": "", + "NAME_PLACEHOLDER": "Namn...", "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", "CHOSE_THEME": "", @@ -514,7 +512,7 @@ "PASSPHRASE_STRENGTH_MODERATE": "", "PASSPHRASE_STRENGTH_STRONG": "", "PREFERENCES": "", - "LANGUAGE": "", + "LANGUAGE": "Språk", "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", @@ -532,8 +530,8 @@ "MONTH": "", "YEAR": "" }, - "COPY_LINK": "", - "DONE": "", + "COPY_LINK": "Kopiera länk", + "DONE": "Klar", "LINK_SHARE_TITLE": "", "REMOVE_LINK": "", "CREATE_PUBLIC_SHARING": "", @@ -579,7 +577,7 @@ "HIDE": "", "UNHIDE": "", "UNHIDE_TO_COLLECTION": "", - "SORT_BY": "", + "SORT_BY": "Sortera efter", "NEWEST_FIRST": "", "OLDEST_FIRST": "", "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", @@ -590,12 +588,11 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", "NEW_YEAR_EVE": "", - "IMAGE": "", + "IMAGE": "Bild", "VIDEO": "", "LIVE_PHOTO": "", "CONVERT": "", @@ -616,10 +613,10 @@ "SAVE_A_COPY_TO_ENTE": "", "RESTORE_ORIGINAL": "", "TRANSFORM": "", - "COLORS": "", + "COLORS": "Färger", "FLIP": "", "ROTATION": "", - "RESET": "", + "RESET": "Återställ", "PHOTO_EDITOR": "", "FASTER_UPLOAD": "", "FASTER_UPLOAD_DESCRIPTION": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/th-TH/translation.json b/web/apps/accounts/public/locales/th-TH/translation.json index bc335bc77..888ed7093 100644 --- a/web/apps/accounts/public/locales/th-TH/translation.json +++ b/web/apps/accounts/public/locales/th-TH/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/tr-TR/translation.json b/web/apps/accounts/public/locales/tr-TR/translation.json index bc335bc77..888ed7093 100644 --- a/web/apps/accounts/public/locales/tr-TR/translation.json +++ b/web/apps/accounts/public/locales/tr-TR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/public/locales/zh-CN/translation.json b/web/apps/accounts/public/locales/zh-CN/translation.json index 15ef565dd..0a72e1c70 100644 --- a/web/apps/accounts/public/locales/zh-CN/translation.json +++ b/web/apps/accounts/public/locales/zh-CN/translation.json @@ -9,7 +9,7 @@ "SIGN_UP": "注册", "NEW_USER": "刚来到 ente", "EXISTING_USER": "现有用户", - "ENTER_NAME": "现有用户", + "ENTER_NAME": "输入名字", "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", "ENTER_EMAIL": "请输入电子邮件地址", "EMAIL_ERROR": "请输入有效的电子邮件", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "您是如何知道Ente的? (可选的)", "REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致", - "CONSOLE_WARNING_STOP": "停止!", - "CONSOLE_WARNING_DESC": "这是专为开发人员设计的浏览器功能。 请不要在此处复制粘贴未经验证的代码。", "CREATE_COLLECTION": "新建相册", "ENTER_ALBUM_NAME": "相册名称", "CLOSE_OPTION": "关闭 (或按Esc键)", @@ -85,9 +83,9 @@ "ZOOM_IN_OUT": "放大/缩小", "PREVIOUS": "上一个 (←)", "NEXT": "下一个 (→)", - "TITLE_PHOTOS": "ente 照片", - "TITLE_ALBUMS": "ente 照片", - "TITLE_AUTH": "ente 验证器", + "TITLE_PHOTOS": "Ente 照片", + "TITLE_ALBUMS": "Ente 照片", + "TITLE_AUTH": "Ente 验证器", "UPLOAD_FIRST_PHOTO": "上传您的第一张照片", "IMPORT_YOUR_FOLDERS": "导入您的文件夹", "UPLOAD_DROPZONE_MESSAGE": "拖放以备份您的文件", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "正在下载 {{name}}", "DOWNLOAD_FAILED": "下载失败", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} 个文件", - "CRASH_REPORTING": "崩溃报告", "CHRISTMAS": "圣诞", "CHRISTMAS_EVE": "平安夜", "NEW_YEAR": "新年", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 cast.ente.io 。", "CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。", "CACHE_DIRECTORY": "缓存文件夹", - "PASSKEYS": "通行密钥", "FREEHAND": "手画", "APPLY_CROP": "应用裁剪", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。", + "PASSKEYS": "通行密钥", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 03d675a2f..513844237 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -1,7 +1,5 @@ -import { setupI18n } from "@/ui/i18n"; -import { CacheProvider } from "@emotion/react"; +import { setupI18n } from "@/next/i18n"; import { APPS, APP_TITLES } from "@ente/shared/apps/constants"; -import { EnteAppProps } from "@ente/shared/apps/types"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import { @@ -15,9 +13,9 @@ import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; -import createEmotionCache from "@ente/shared/themes/createEmotionCache"; import { CssBaseline, useMediaQuery } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; +import { AppProps } from "next/app"; import Head from "next/head"; import { useRouter } from "next/router"; import { createContext, useEffect, useState } from "react"; @@ -31,10 +29,7 @@ interface AppContextProps { export const AppContext = createContext({} as AppContextProps); -// Client-side cache, shared for the whole session of the user in the browser. -const clientSideEmotionCache = createEmotionCache(); - -export default function App(props: EnteAppProps) { +export default function App(props: AppProps) { const [isI18nReady, setIsI18nReady] = useState(false); const [showNavbar, setShowNavBar] = useState(false); @@ -54,11 +49,7 @@ export default function App(props: EnteAppProps) { const router = useRouter(); - const { - Component, - emotionCache = clientSideEmotionCache, - pageProps, - } = props; + const { Component, pageProps } = props; const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK); @@ -87,7 +78,7 @@ export default function App(props: EnteAppProps) { // TODO: Localise APP_TITLES return ( - + <> {APP_TITLES.get(APPS.ACCOUNTS)} )} {showNavbar && } - + {isI18nReady && } - + ); } diff --git a/web/apps/accounts/src/pages/_document.tsx b/web/apps/accounts/src/pages/_document.tsx index 09d4d5782..3c6c2a959 100644 --- a/web/apps/accounts/src/pages/_document.tsx +++ b/web/apps/accounts/src/pages/_document.tsx @@ -1,7 +1,3 @@ -import DocumentPage, { - EnteDocumentProps, -} from "@ente/shared/next/pages/_document"; +import DocumentPage from "@ente/shared/next/pages/_document"; -export default function Document(props: EnteDocumentProps) { - return ; -} +export default DocumentPage; diff --git a/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx new file mode 100644 index 000000000..aea836f6e --- /dev/null +++ b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx @@ -0,0 +1,19 @@ +import { TwoFactorType } from "@ente/accounts/constants/twofactor"; +import RecoverPage from "@ente/accounts/pages/recover"; +import { APPS } from "@ente/shared/apps/constants"; +import { useRouter } from "next/router"; +import { AppContext } from "pages/_app"; +import { useContext } from "react"; + +export default function Recover() { + const appContext = useContext(AppContext); + const router = useRouter(); + return ( + + ); +} diff --git a/web/apps/accounts/src/pages/passkeys/flow/index.tsx b/web/apps/accounts/src/pages/passkeys/flow/index.tsx index 517777b9c..1f082bf6b 100644 --- a/web/apps/accounts/src/pages/passkeys/flow/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/flow/index.tsx @@ -257,6 +257,18 @@ const PasskeysFlow = () => { > {t("TRY_AGAIN")} + + {t("RECOVER_TWO_FACTOR")} + diff --git a/web/apps/accounts/src/styles/global.css b/web/apps/accounts/src/styles/global.css index 0ea6c125d..98ad85a9b 100644 --- a/web/apps/accounts/src/styles/global.css +++ b/web/apps/accounts/src/styles/global.css @@ -150,21 +150,6 @@ body { background-color: #51cd7c; } -.carousel-inner { - padding-bottom: 50px !important; -} - -.carousel-indicators li { - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 12px; -} - -.carousel-indicators .active { - background-color: #51cd7c; -} - div.otp-input input { width: 36px !important; height: 36px; diff --git a/web/apps/auth/public/locales/bg-BG/translation.json b/web/apps/auth/public/locales/bg-BG/translation.json new file mode 100644 index 000000000..03faf16c2 --- /dev/null +++ b/web/apps/auth/public/locales/bg-BG/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "
Личен бекъп
на твоите спомени
", + "HERO_SLIDE_1": "Криптиран от край до край по подразбиране", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/auth/public/locales/de-DE/translation.json b/web/apps/auth/public/locales/de-DE/translation.json index b09446dc8..9a1c4073c 100644 --- a/web/apps/auth/public/locales/de-DE/translation.json +++ b/web/apps/auth/public/locales/de-DE/translation.json @@ -31,80 +31,78 @@ "VERIFY_PASSPHRASE": "Einloggen", "INCORRECT_PASSPHRASE": "Falsches Passwort", "ENTER_ENC_PASSPHRASE": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", - "PASSPHRASE_DISCLAIMER": "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.", + "PASSPHRASE_DISCLAIMER": "Wir speichern dein Passwort nicht. Wenn du es vergisst, können wir dir nicht helfen, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", "WELCOME_TO_ENTE_HEADING": "Willkommen bei
", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", + "WELCOME_TO_ENTE_SUBHEADING": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Wo deine besten Fotos leben", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...", "PASSPHRASE_HINT": "Passwort", "CONFIRM_PASSPHRASE": "Passwort bestätigen", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", + "REFERRAL_CODE_HINT": "Wie hast du von Ente erfahren? (optional)", + "REFERRAL_INFO": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein", - "CONSOLE_WARNING_STOP": "STOPP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", "CREATE_COLLECTION": "Neues Album", "ENTER_ALBUM_NAME": "Albumname", "CLOSE_OPTION": "Schließen (Esc)", "ENTER_FILE_NAME": "Dateiname", "CLOSE": "Schließen", "NO": "Nein", - "NOTHING_HERE": "Nothing to see here yet 👀", + "NOTHING_HERE": "Hier gibt es noch nichts zu sehen 👀", "UPLOAD": "Hochladen", "IMPORT": "Importieren", "ADD_PHOTOS": "Fotos hinzufügen", "ADD_MORE_PHOTOS": "Mehr Fotos hinzufügen", - "add_photos_one": "Add 1 item", - "add_photos_other": "Add {{count, number}} items", + "add_photos_one": "Eine Datei hinzufügen", + "add_photos_other": "{{count, number}} Dateien hinzufügen", "SELECT_PHOTOS": "Foto auswählen", "FILE_UPLOAD": "Datei hochladen", "UPLOAD_STAGE_MESSAGE": { "0": "Hochladen wird vorbereitet", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", + "1": "Lese Google-Metadaten", + "2": "Metadaten von {{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien extrahiert", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien verarbeitet", + "4": "Verbleibende Uploads werden abgebrochen", "5": "Sicherung abgeschlossen" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", + "FILE_NOT_UPLOADED_LIST": "Die folgenden Dateien wurden nicht hochgeladen", "SUBSCRIPTION_EXPIRED": "Abonnement abgelaufen", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Dein Abonnement ist abgelaufen, bitte erneuere es", "STORAGE_QUOTA_EXCEEDED": "Speichergrenze überschritten", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", - "NO_ACCOUNT": "Don't have an account", - "ACCOUNT_EXISTS": "Already have an account", + "INITIAL_LOAD_DELAY_WARNING": "Das erste Laden kann einige Zeit in Anspruch nehmen", + "USER_DOES_NOT_EXIST": "Leider konnte kein Benutzer mit dieser E-Mail gefunden werden", + "NO_ACCOUNT": "Kein Konto vorhanden", + "ACCOUNT_EXISTS": "Es ist bereits ein Account vorhanden", "CREATE": "Erstellen", "DOWNLOAD": "Herunterladen", "DOWNLOAD_OPTION": "Herunterladen (D)", "DOWNLOAD_FAVORITES": "Favoriten herunterladen", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", + "DOWNLOAD_UNCATEGORIZED": "Download unkategorisiert", + "DOWNLOAD_HIDDEN_ITEMS": "Versteckte Dateien herunterladen", "COPY_OPTION": "Als PNG kopieren (Strg / Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", + "TOGGLE_FULLSCREEN": "Vollbild umschalten (F)", "ZOOM_IN_OUT": "Herein-/Herauszoomen", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", + "PREVIOUS": "Vorherige (←)", + "NEXT": "Weitere (→)", + "TITLE_PHOTOS": "Ente Fotos", + "TITLE_ALBUMS": "Ente Fotos", "TITLE_AUTH": "Ente Auth", "UPLOAD_FIRST_PHOTO": "Lade dein erstes Foto hoch", "IMPORT_YOUR_FOLDERS": "Importiere deiner Ordner", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", + "UPLOAD_DROPZONE_MESSAGE": "Loslassen, um Dateien zu sichern", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Loslassen, um beobachteten Ordner hinzuzufügen", "TRASH_FILES_TITLE": "Dateien löschen?", "TRASH_FILE_TITLE": "Datei löschen?", "DELETE_FILES_TITLE": "Sofort löschen?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", + "DELETE_FILES_MESSAGE": "Ausgewählte Dateien werden dauerhaft aus Ihrem Ente-Konto gelöscht.", "DELETE": "Löschen", "DELETE_OPTION": "Löschen (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", + "FAVORITE_OPTION": "Zu Favoriten hinzufügen (L)", + "UNFAVORITE_OPTION": "Von Favoriten entfernen (L)", + "MULTI_FOLDER_UPLOAD": "Mehrere Ordner erkannt", + "UPLOAD_STRATEGY_CHOICE": "Möchtest du sie hochladen in", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Ein einzelnes Album", "OR": "oder", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Getrennte Alben", "SESSION_EXPIRED_MESSAGE": "Ihre Sitzung ist abgelaufen. Bitte loggen Sie sich erneut ein, um fortzufahren", "SESSION_EXPIRED": "Sitzung abgelaufen", "PASSWORD_GENERATION_FAILED": "Dein Browser konnte keinen starken Schlüssel generieren, der den Verschlüsselungsstandards des Entes entspricht, bitte versuche die mobile App oder einen anderen Browser zu verwenden", @@ -113,9 +111,9 @@ "RECOVERY_KEY": "Wiederherstellungsschlüssel", "SAVE_LATER": "Auf später verschieben", "SAVE": "Schlüssel speichern", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", + "RECOVERY_KEY_DESCRIPTION": "Falls du dein Passwort vergisst, kannst du deine Daten nur mit diesem Schlüssel wiederherstellen.", + "RECOVER_KEY_GENERATION_FAILED": "Wiederherstellungsschlüssel konnte nicht generiert werden, bitte versuche es erneut", + "KEY_NOT_STORED_DISCLAIMER": "Wir speichern diesen Schlüssel nicht, also speichere ihn bitte an einem sicheren Ort", "FORGOT_PASSWORD": "Passwort vergessen", "RECOVER_ACCOUNT": "Konto wiederherstellen", "RECOVERY_KEY_HINT": "Wiederherstellungsschlüssel", @@ -132,15 +130,15 @@ "CANCEL": "Abbrechen", "LOGOUT": "Ausloggen", "DELETE_ACCOUNT": "Konto löschen", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", + "DELETE_ACCOUNT_MESSAGE": "

Bitte sende eine E-Mail an {{emailID}} mit deiner registrierten E-Mail-Adresse.

Deine Anfrage wird innerhalb von 72 Stunden bearbeitet.

", "LOGOUT_MESSAGE": "Sind sie sicher, dass sie sich ausloggen möchten?", "CHANGE_EMAIL": "E-Mail-Adresse ändern", "OK": "OK", "SUCCESS": "Erfolgreich", "ERROR": "Fehler", "MESSAGE": "Nachricht", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", + "INSTALL_MOBILE_APP": "Installiere unsere Android oder iOS App, um automatisch alle deine Fotos zu sichern", + "DOWNLOAD_APP_MESSAGE": "Entschuldigung, dieser Vorgang wird derzeit nur von unserer Desktop-App unterstützt", "DOWNLOAD_APP": "Desktopanwendung herunterladen", "EXPORT": "Daten exportieren", "SUBSCRIPTION": "Abonnement", @@ -154,37 +152,37 @@ "MANAGE_PLAN": "Verwalte dein Abonnement", "ACTIVE": "Aktiv", "OFFLINE_MSG": "Du bist offline, gecachte Erinnerungen werden angezeigt", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", + "FREE_SUBSCRIPTION_INFO": "Du bist auf dem kostenlosen Plan, der am {{date, dateTime}} ausläuft", "FAMILY_SUBSCRIPTION_INFO": "Sie haben einen Familienplan verwaltet von", "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Erneuert am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Endet am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Ihr Abo endet am {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Dein {{storage, string}} Add-on ist gültig bis {{date, dateTime}}", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Sie haben Ihr Speichervolumen überschritten, bitte upgraden Sie", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", + "SUBSCRIPTION_PURCHASE_SUCCESS": "

Wir haben deine Zahlung erhalten

Dein Abonnement ist gültig bis {{date, dateTime}}

", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Dein Kauf wurde abgebrochen. Bitte versuche es erneut, wenn du abonnieren willst", "SUBSCRIPTION_PURCHASE_FAILED": "Kauf des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", "SUBSCRIPTION_UPDATE_FAILED": "Aktualisierung des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Es tut uns leid, die Zahlung ist fehlgeschlagen, als wir versuchten Ihre Karte zu belasten. Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut", + "STRIPE_AUTHENTICATION_FAILED": "Wir können deine Zahlungsmethode nicht authentifizieren. Bitte wähle eine andere Zahlungsmethode und versuche es erneut", "UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren", "MONTHLY": "Monatlich", "YEARLY": "Jährlich", "UPDATE_SUBSCRIPTION_MESSAGE": "Sind Sie sicher, dass Sie Ihren Tarif ändern möchten?", "UPDATE_SUBSCRIPTION": "Plan ändern", "CANCEL_SUBSCRIPTION": "Abonnement kündigen", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", + "CANCEL_SUBSCRIPTION_MESSAGE": "

Alle deine Daten werden am Ende dieses Abrechnungszeitraums von unseren Servern gelöscht.

Bist du sicher, dass du dein Abonnement kündigen möchtest?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Bist du sicher, dass du dein Abonnement beenden möchtest?

", + "SUBSCRIPTION_CANCEL_FAILED": "Abonnement konnte nicht storniert werden", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement erfolgreich beendet", "REACTIVATE_SUBSCRIPTION": "Abonnement reaktivieren", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Nach der Reaktivierung wird am {{date, dateTime}} abgerechnet", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Abonnement erfolgreich aktiviert ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Reaktivierung der Abonnementverlängerung fehlgeschlagen", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Vielen Dank", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Mobiles Abonnement kündigen", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Bitte kündige dein Abonnement in der mobilen App, um hier ein Abonnement zu aktivieren", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Bitte kontaktiere uns über {{emailID}}, um dein Abo zu verwalten", "RENAME": "Umbenennen", "RENAME_FILE": "Datei umbenennen", "RENAME_COLLECTION": "Album umbenennen", @@ -198,41 +196,41 @@ "SHAREES": "Geteilt mit", "SHARE_WITH_SELF": "Du kannst nicht mit dir selbst teilen", "ALREADY_SHARED": "Hoppla, Sie teilen dies bereits mit {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", + "SHARING_BAD_REQUEST_ERROR": "Albumfreigabe nicht erlaubt", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Freigabe ist für kostenlose Konten deaktiviert", "DOWNLOAD_COLLECTION": "Album herunterladen", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", + "DOWNLOAD_COLLECTION_MESSAGE": "

Bist du sicher, dass du das komplette Album herunterladen möchtest?

Alle Dateien werden der Warteschlange zum sequenziellen Download hinzugefügt

", + "CREATE_ALBUM_FAILED": "Fehler beim Erstellen des Albums, bitte versuche es erneut", "SEARCH": "Suchen", "SEARCH_RESULTS": "Ergebnisse durchsuchen", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "NO_RESULTS": "Keine Ergebnisse gefunden", + "SEARCH_HINT": "Suche nach Alben, Datum, Beschreibungen, ...", "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Standort", - "CITY": "Location", + "CITY": "Ort", "DATE": "Datum", "FILE_NAME": "Dateiname", "THING": "Inhalt", "FILE_CAPTION": "Beschreibung", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "FILE_TYPE": "Dateityp", + "CLIP": "Magie" }, "photos_count_zero": "Keine Erinnerungen", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", + "photos_count_one": "Eine Erinnerung", + "photos_count_other": "{{count, number}} Erinnerungen", + "TERMS_AND_CONDITIONS": "Ich stimme den Bedingungen und Datenschutzrichtlinien zu", "ADD_TO_COLLECTION": "Zum Album hinzufügen", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", + "SELECTED": "ausgewählt", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Dieses Video kann in deinem Browser nicht abgespielt werden", "PEOPLE": "Personen", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", + "INDEXING_SCHEDULED": "Indizierung ist geplant...", + "ANALYZING_PHOTOS": "Indiziere Fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Indiziere Personen in {{indexStatus.nSyncedFiles,number}} Fotos...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles,number}} Fotos wurden indiziert", + "UNIDENTIFIED_FACES": "unidentifizierte Gesichter", + "OBJECTS": "Objekte", + "TEXT": "Text", "INFO": "Info ", "INFO_OPTION": "Info (I)", "FILE_NAME": "Dateiname", @@ -244,8 +242,8 @@ "ENABLE_MAPS": "Karten aktivieren?", "ENABLE_MAP": "Karte aktivieren", "DISABLE_MAPS": "Karten deaktivieren?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", + "ENABLE_MAP_DESCRIPTION": "

Dies wird deine Fotos auf einer Weltkarte anzeigen.

Die Karte wird von OpenStreetMap gehostet und die genauen Standorte deiner Fotos werden niemals geteilt.

Diese Funktion kannst du jederzeit in den Einstellungen deaktivieren.

", + "DISABLE_MAP_DESCRIPTION": "

Dies wird die Anzeige deiner Fotos auf einer Weltkarte deaktivieren.

Du kannst diese Funktion jederzeit in den Einstellungen aktivieren.

", "DISABLE_MAP": "Karte deaktivieren", "DETAILS": "Details", "VIEW_EXIF": "Alle EXIF-Daten anzeigen", @@ -254,67 +252,67 @@ "ISO": "ISO", "TWO_FACTOR": "Zwei-Faktor", "TWO_FACTOR_AUTHENTICATION": "Zwei-Faktor-Authentifizierung", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", + "TWO_FACTOR_QR_INSTRUCTION": "Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App", "ENTER_CODE_MANUALLY": "Geben Sie den Code manuell ein", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Bitte gib diesen Code in deiner bevorzugten Authentifizierungs-App ein", "SCAN_QR_CODE": "QR‐Code stattdessen scannen", "ENABLE_TWO_FACTOR": "Zwei-Faktor-Authentifizierung aktivieren", "ENABLE": "Aktivieren", - "LOST_DEVICE": "Lost two-factor device", + "LOST_DEVICE": "Zwei-Faktor-Gerät verloren", "INCORRECT_CODE": "Falscher Code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", + "TWO_FACTOR_INFO": "Fügen Sie eine zusätzliche Sicherheitsebene hinzu, indem Sie mehr als Ihre E-Mail und Ihr Passwort benötigen, um sich mit Ihrem Account anzumelden", "DISABLE_TWO_FACTOR_LABEL": "Deaktiviere die Zwei-Faktor-Authentifizierung", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", + "UPDATE_TWO_FACTOR_LABEL": "Authentifizierungsgerät aktualisieren", "DISABLE": "Deaktivieren", "RECONFIGURE": "Neu einrichten", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", + "UPDATE_TWO_FACTOR": "Zweiten Faktor aktualisieren", + "UPDATE_TWO_FACTOR_MESSAGE": "Fahren Sie fort, werden alle Ihre zuvor konfigurierten Authentifikatoren ungültig", + "UPDATE": "Aktualisierung", + "DISABLE_TWO_FACTOR": "Zweiten Faktor deaktivieren", + "DISABLE_TWO_FACTOR_MESSAGE": "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst", + "TWO_FACTOR_DISABLE_FAILED": "Fehler beim Deaktivieren des zweiten Faktors, bitte versuchen Sie es erneut", "EXPORT_DATA": "Daten exportieren", "SELECT_FOLDER": "Ordner auswählen", "DESTINATION": "Zielort", "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "LAST_EXPORT_TIME": "Letztes Exportdatum", + "EXPORT_AGAIN": "Neusynchronisation", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", "SEND_OTT": "OTP senden", "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "Fehlgeschlagene Uploads erneut probieren", "FAILED_UPLOADS": "Fehlgeschlagene Uploads ", "SKIPPED_FILES": "Ignorierte Uploads", "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Das Vorschaubild konnte nicht erzeugt werden", "UNSUPPORTED_FILES": "Nicht unterstützte Dateien", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", + "SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads", + "SKIPPED_INFO": "", "UNSUPPORTED_INFO": "ente unterstützt diese Dateiformate noch nicht", "BLOCKED_UPLOADS": "Blockierte Uploads", "SKIPPED_VIDEOS": "Übersprungene Videos", "INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung", - "INPROGRESS_UPLOADS": "Uploads in progress", + "INPROGRESS_UPLOADS": "Upload läuft", "TOO_LARGE_UPLOADS": "Große Dateien", "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Zu wenig Speicher", "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie die maximale Größe für Ihren Speicherplan überschreiten", "TOO_LARGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", "THUMBNAIL_GENERATION_FAILED_INFO": "Diese Dateien wurden hochgeladen, aber leider konnten wir nicht die Thumbnails für sie generieren.", "UPLOAD_TO_COLLECTION": "In Album hochladen", - "UNCATEGORIZED": "Uncategorized", + "UNCATEGORIZED": "Unkategorisiert", "ARCHIVE": "Archiv", "FAVORITES": "Favoriten", "ARCHIVE_COLLECTION": "Album archivieren", "ARCHIVE_SECTION_NAME": "Archiv", "ALL_SECTION_NAME": "Alle", "MOVE_TO_COLLECTION": "Zum Album verschieben", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", + "UNARCHIVE": "Dearchivieren", + "UNARCHIVE_COLLECTION": "Album dearchivieren", + "HIDE_COLLECTION": "Album ausblenden", + "UNHIDE_COLLECTION": "Album wieder einblenden", "MOVE": "Verschieben", "ADD": "Hinzufügen", "REMOVE": "Entfernen", @@ -322,204 +320,204 @@ "REMOVE_FROM_COLLECTION": "Aus Album entfernen", "TRASH": "Papierkorb", "MOVE_TO_TRASH": "In Papierkorb verschieben", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", "DELETE_PERMANENTLY": "Dauerhaft löschen", "RESTORE": "Wiederherstellen", "RESTORE_TO_COLLECTION": "In Album wiederherstellen", "EMPTY_TRASH": "Papierkorb leeren", "EMPTY_TRASH_TITLE": "Papierkorb leeren?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", + "EMPTY_TRASH_MESSAGE": "", "LEAVE_SHARED_ALBUM": "Ja, verlassen", "LEAVE_ALBUM": "Album verlassen", "LEAVE_SHARED_ALBUM_TITLE": "Geteiltes Album verlassen?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", + "LEAVE_SHARED_ALBUM_MESSAGE": "", "NOT_FILE_OWNER": "Dateien in einem freigegebenen Album können nicht gelöscht werden", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", "SORT_BY_CREATION_TIME_ASCENDING": "Ältestem", "SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert", "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", + "COMPRESS_THUMBNAILS": "Vorschaubilder komprimieren", + "THUMBNAIL_REPLACED": "Vorschaubilder komprimiert", "FIX_THUMBNAIL": "Komprimiere", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", + "FIX_THUMBNAIL_LATER": "Später komprimieren", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "Zeit reparieren", + "FIX_CREATION_TIME_IN_PROGRESS": "Zeit wird repariert", + "CREATION_TIME_UPDATED": "Datei-Zeit aktualisiert", + "UPDATE_CREATION_TIME_NOT_STARTED": "Wählen Sie die Option, die Sie verwenden möchten", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "Benutzerdefinierte Zeit", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Fehler beim Öffnen der Pläne", "INSTALL": "Installieren", "SHARING_DETAILS": "Details teilen", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", + "MODIFY_SHARING": "Freigabe ändern", + "ADD_COLLABORATORS": "Bearbeiter hinzufügen", + "ADD_NEW_EMAIL": "Neue E-Mail-Adresse hinzufügen", + "shared_with_people_zero": "Mit bestimmten Personen teilen", + "shared_with_people_one": "Geteilt mit einer Person", + "shared_with_people_other": "Geteilt mit {{count, number}} Personen", + "participants_zero": "Keine Teilnehmer", + "participants_one": "1 Teilnehmer", + "participants_other": "{{count, number}} Teilnehmer", + "ADD_VIEWERS": "Betrachter hinzufügen", + "PARTICIPANTS": "Teilnehmer", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "Berechtigung ändern?", "REMOVE_PARTICIPANT": "Entfernen?", "CONFIRM_REMOVE": "Ja, entfernen", "MANAGE": "Verwalten", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", + "ADDED_AS": "Hinzugefügt als", + "COLLABORATOR_RIGHTS": "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen", "REMOVE_PARTICIPANT_HEAD": "Teilnehmer entfernen", "OWNER": "Besitzer", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", + "COLLABORATORS": "Bearbeiter", + "ADD_MORE": "Mehr hinzufügen", + "VIEWERS": "Zuschauer", + "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", + "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - Nicht gefunden", "LINK_EXPIRED": "Link ist abgelaufen", "LINK_EXPIRED_MESSAGE": "Dieser Link ist abgelaufen oder wurde deaktiviert!", - "MANAGE_LINK": "Manage link", + "MANAGE_LINK": "Link verwalten", "LINK_TOO_MANY_REQUESTS": "Sorry, dieses Album wurde auf zu vielen Geräten angezeigt!", "FILE_DOWNLOAD": "Downloads erlauben", "LINK_PASSWORD_LOCK": "Passwort Sperre", - "PUBLIC_COLLECT": "Allow adding photos", + "PUBLIC_COLLECT": "Hinzufügen von Fotos erlauben", "LINK_DEVICE_LIMIT": "Geräte Limit", - "NO_DEVICE_LIMIT": "None", + "NO_DEVICE_LIMIT": "Keins", "LINK_EXPIRY": "Ablaufdatum des Links", "NEVER": "Niemals", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", + "DISABLE_FILE_DOWNLOAD": "Download deaktivieren", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "Enthält schädliche Inhalte", + "COPYRIGHT": "Verletzung des Urheberrechts von jemandem, den ich repräsentieren darf", + "SHARED_USING": "Freigegeben über ", "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", + "SHARING_REFERRAL_CODE": "", "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", + "DISABLE_PASSWORD": "Passwort-Sperre deaktivieren", + "DISABLE_PASSWORD_MESSAGE": "Sind Sie sicher, dass Sie die Passwort-Sperre deaktivieren möchten?", "PASSWORD_LOCK": "Passwort Sperre", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", + "LOCK": "Sperren", + "DOWNLOAD_UPLOAD_LOGS": "Debug-Logs", "UPLOAD_FILES": "Datei", "UPLOAD_DIRS": "Ordner", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", - "DEDUPLICATE_FILES": "Deduplicate files", + "DEDUPLICATE_FILES": "", "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "Du hast keine Duplikate, die gelöscht werden können", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", + "CLUB_BY_CAPTURE_TIME": "", "FILES": "Dateien", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", "STOP_UPLOADS_HEADER": "Hochladen stoppen?", "YES_STOP_UPLOADS": "Ja, Hochladen stoppen", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", + "albums_other": "", "ALL_ALBUMS": "Alle Alben", "ALBUMS": "Alben", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "Gib den 6-stelligen Code aus\ndeiner Authentifizierungs-App ein.", "CREATE_ACCOUNT": "Account erstellen", "COPIED": "Kopiert", "CANVAS_BLOCKED_TITLE": "Vorschaubild konnte nicht erstellt werden", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", "UPGRADE_NOW": "Jetzt upgraden", - "RENEW_NOW": "Renew now", + "RENEW_NOW": "", "STORAGE": "Speicher", "USED": "verwendet", "YOU": "Sie", "FAMILY": "Familie", "FREE": "frei", "OF": "von", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", "ADD_FOLDER": "Ordner hinzufügen", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", "YES_STOP": "Ja, Stopp", - "MONTH_SHORT": "mo", + "MONTH_SHORT": "", "YEAR": "Jahr", "FAMILY_PLAN": "Familientarif", "DOWNLOAD_LOGS": "Logs herunterladen", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "Ordner ändern", + "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", "GB": "GB", "POPULAR": "Beliebt", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", + "FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren", "FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", + "CURRENT_USAGE": "Aktuelle Nutzung ist {{usage}}", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", "AUTHENTICATE": "Authentifizieren", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", "NEVERMIND": "Egal", "UPDATE_AVAILABLE": "Neue Version verfügbar", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", + "UPDATE_INSTALLABLE_MESSAGE": "", "INSTALL_NOW": "Jetzt installieren", "INSTALL_ON_NEXT_LAUNCH": "Beim nächsten Start installieren", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", "IGNORE_THIS_VERSION": "Diese Version ignorieren", "TODAY": "Heute", "YESTERDAY": "Gestern", "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", "DISABLE_BETA": "Beta deaktivieren", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", "ADVANCED": "Erweitert", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", "PASSPHRASE_STRENGTH_WEAK": "Passwortstärke: Schwach", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", + "PASSPHRASE_STRENGTH_MODERATE": "", "PASSPHRASE_STRENGTH_STRONG": "Passwortstärke: Stark", "PREFERENCES": "Einstellungen", "LANGUAGE": "Sprache", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", + "B": "", "KB": "KB", "MB": "MB", "GB": "GB", @@ -539,46 +537,46 @@ "CREATE_PUBLIC_SHARING": "Öffentlichen Link erstellen", "PUBLIC_LINK_CREATED": "Öffentlicher Link erstellt", "PUBLIC_LINK_ENABLED": "Öffentlicher Link aktiviert", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { "START": "Export gestartet", - "IN_PROGRESS": "Export already in progress", + "IN_PROGRESS": "", "FINISH": "Export abgeschlossen", - "UP_TO_DATE": "No new files to export" + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", "CONFIRM_DELETE_ACCOUNT": "Kontolöschung bestätigen", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", "AUTH_NEXT": "Weiter", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", + "AUTH_DOWNLOAD_MOBILE_APP": "", "HIDDEN": "Versteckt", "HIDE": "Ausblenden", "UNHIDE": "Einblenden", - "UNHIDE_TO_COLLECTION": "Unhide to album", + "UNHIDE_TO_COLLECTION": "", "SORT_BY": "Sortieren nach", "NEWEST_FIRST": "Neueste zuerst", "OLDEST_FIRST": "Älteste zuerst", @@ -586,59 +584,71 @@ "SELECT_COLLECTION": "Album auswählen", "PIN_ALBUM": "Album anheften", "UNPIN_ALBUM": "Album lösen", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/en-US/translation.json b/web/apps/auth/public/locales/en-US/translation.json index 6870df319..de8d2fe2a 100644 --- a/web/apps/auth/public/locales/en-US/translation.json +++ b/web/apps/auth/public/locales/en-US/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", "CREATE_COLLECTION": "New album", "ENTER_ALBUM_NAME": "Album name", "CLOSE_OPTION": "Close (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Downloading {{name}}", "DOWNLOAD_FAILED": "Download failed", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", "CHRISTMAS": "Christmas", "CHRISTMAS_EVE": "Christmas Eve", "NEW_YEAR": "New Year", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", "FREEHAND": "Freehand", "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.", + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Delete passkey", + "DELETE_PASSKEY_CONFIRMATION": "Are you sure you want to delete this passkey? This action is irreversible.", + "RENAME_PASSKEY": "Rename passkey", + "ADD_PASSKEY": "Add passkey", + "ENTER_PASSKEY_NAME": "Enter passkey name", + "PASSKEYS_DESCRIPTION": "Passkeys are a modern and secure second-factor for your Ente account. They use on-device biometric authentication for convenience and security.", + "CREATED_AT": "Created at", + "PASSKEY_LOGIN_FAILED": "Passkey login failed", + "PASSKEY_LOGIN_URL_INVALID": "The login URL is invalid.", + "PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.", + "TRY_AGAIN": "Try again", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.", + "LOGIN_WITH_PASSKEY": "Login with passkey" } diff --git a/web/apps/auth/public/locales/es-ES/translation.json b/web/apps/auth/public/locales/es-ES/translation.json index 05f3d0325..a29165e4e 100644 --- a/web/apps/auth/public/locales/es-ES/translation.json +++ b/web/apps/auth/public/locales/es-ES/translation.json @@ -38,11 +38,9 @@ "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generando claves de encriptación...", "PASSPHRASE_HINT": "Contraseña", "CONFIRM_PASSPHRASE": "Confirmar contraseña", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Esta es una característica del navegador destinada a los desarrolladores. Por favor, no copie y pegue código sin verificar aquí.", "CREATE_COLLECTION": "Nuevo álbum", "ENTER_ALBUM_NAME": "Nombre del álbum", "CLOSE_OPTION": "Cerrar (Esc)", @@ -79,7 +77,7 @@ "DOWNLOAD_OPTION": "Descargar (D)", "DOWNLOAD_FAVORITES": "Descargar favoritos", "DOWNLOAD_UNCATEGORIZED": "Descargar no categorizados", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", + "DOWNLOAD_HIDDEN_ITEMS": "", "COPY_OPTION": "Copiar como PNG (Ctrl/Cmd - C)", "TOGGLE_FULLSCREEN": "Alternar pantalla completa (F)", "ZOOM_IN_OUT": "Acercar/alejar", @@ -159,7 +157,7 @@ "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Se renueva en {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Termina el {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Tu suscripción será cancelada el {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Ha excedido su cuota de almacenamiento, por favor actualice", "SUBSCRIPTION_PURCHASE_SUCCESS": "

Hemos recibido tu pago

¡Tu suscripción es válida hasta {{date, dateTime}}

", "SUBSCRIPTION_PURCHASE_CANCELLED": "Tu compra ha sido cancelada, por favor inténtalo de nuevo si quieres suscribirte", @@ -174,7 +172,7 @@ "UPDATE_SUBSCRIPTION": "Cambiar de plan", "CANCEL_SUBSCRIPTION": "Cancelar suscripción", "CANCEL_SUBSCRIPTION_MESSAGE": "

Todos tus datos serán eliminados de nuestros servidores al final de este periodo de facturación.

¿Está seguro de que desea cancelar su suscripción?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", "SUBSCRIPTION_CANCEL_FAILED": "No se pudo cancelar la suscripción", "SUBSCRIPTION_CANCEL_SUCCESS": "Suscripción cancelada correctamente", "REACTIVATE_SUBSCRIPTION": "Reactivar la suscripción", @@ -210,13 +208,13 @@ "SEARCH_TYPE": { "COLLECTION": "Álbum", "LOCATION": "Localización", - "CITY": "Location", + "CITY": "", "DATE": "Fecha", "FILE_NAME": "Nombre del archivo", "THING": "Contenido", "FILE_CAPTION": "Descripción", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "FILE_TYPE": "", + "CLIP": "" }, "photos_count_zero": "No hay recuerdos", "photos_count_one": "1 recuerdo", @@ -239,14 +237,14 @@ "CAPTION_PLACEHOLDER": "Añadir una descripción", "LOCATION": "Localización", "SHOW_ON_MAP": "Ver en OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", "DETAILS": "Detalles", "VIEW_EXIF": "Ver todos los datos de EXIF", "NO_EXIF": "No hay datos EXIF", @@ -313,8 +311,8 @@ "MOVE_TO_COLLECTION": "Mover al álbum", "UNARCHIVE": "Desarchivar", "UNARCHIVE_COLLECTION": "Desarchivar álbum", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", "MOVE": "Mover", "ADD": "Añadir", "REMOVE": "Eliminar", @@ -357,40 +355,40 @@ "CAPTION_CHARACTER_LIMIT": "Máximo 5000 caracteres", "DATE_TIME_ORIGINAL": "EXIF: Fecha original", "DATE_TIME_DIGITIZED": "EXIF: Fecha Digitalizado", - "METADATA_DATE": "EXIF:MetadataDate", + "METADATA_DATE": "", "CUSTOM_TIME": "Hora personalizada", "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planes", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Error al abrir los planes", "INSTALL": "Instalar", "SHARING_DETAILS": "Compartir detalles", "MODIFY_SHARING": "Modificar compartir", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", "OWNER": "Propietario", "COLLABORATORS": "Colaboradores", "ADD_MORE": "Añadir más", - "VIEWERS": "Viewers", + "VIEWERS": "", "OR_ADD_EXISTING": "O elige uno existente", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", + "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - No Encontrado", "LINK_EXPIRED": "Enlace expirado", "LINK_EXPIRED_MESSAGE": "Este enlace ha caducado o ha sido desactivado!", @@ -436,10 +434,10 @@ "albums_other": "{{count}} álbumes", "ALL_ALBUMS": "Todos los álbumes", "ALBUMS": "Álbumes", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "Ingrese el código de seis dígitos de su aplicación de autenticación a continuación.", "CREATE_ACCOUNT": "Crear cuenta", "COPIED": "Copiado", @@ -543,10 +541,10 @@ "PUBLIC_COLLECT_SUBTEXT": "Permitir a las personas con el enlace añadir fotos al álbum compartido.", "STOP_EXPORT": "Stop", "EXPORT_PROGRESS": "{{progress.success}} / {{progress.total}} archivos exportados", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { "START": "Exportar iniciando", "IN_PROGRESS": "Exportación ya en curso", @@ -575,70 +573,82 @@ "at": "a las", "AUTH_NEXT": "siguiente", "AUTH_DOWNLOAD_MOBILE_APP": "Descarga nuestra aplicación móvil para administrar tus secretos", - "HIDDEN": "Hidden", + "HIDDEN": "", "HIDE": "Ocultar", "UNHIDE": "Mostrar", "UNHIDE_TO_COLLECTION": "Hacer visible al álbum", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", "TRANSFORM": "Transformar", "COLORS": "Colores", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/fa-IR/translation.json b/web/apps/auth/public/locales/fa-IR/translation.json index 2c36eda0c..2d21fcb3d 100644 --- a/web/apps/auth/public/locales/fa-IR/translation.json +++ b/web/apps/auth/public/locales/fa-IR/translation.json @@ -1,644 +1,654 @@ { - "HERO_SLIDE_1_TITLE": "
Private backups
for your memories
", - "HERO_SLIDE_1": "End-to-end encrypted by default", - "HERO_SLIDE_2_TITLE": "
Safely stored
at a fallout shelter
", - "HERO_SLIDE_2": "Designed to outlive", - "HERO_SLIDE_3_TITLE": "
Available
everywhere
", - "HERO_SLIDE_3": "Android, iOS, Web, Desktop", - "LOGIN": "Login", - "SIGN_UP": "Signup", - "NEW_USER": "New to ente", - "EXISTING_USER": "Existing user", - "ENTER_NAME": "Enter name", - "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", - "ENTER_EMAIL": "Enter email address", - "EMAIL_ERROR": "Enter a valid email", - "REQUIRED": "Required", - "EMAIL_SENT": "Verification code sent to {{email}}", - "CHECK_INBOX": "Please check your inbox (and spam) to complete verification", - "ENTER_OTT": "Verification code", - "RESEND_MAIL": "Resend code", - "VERIFY": "Verify", - "UNKNOWN_ERROR": "Something went wrong, please try again", - "INVALID_CODE": "Invalid verification code", - "EXPIRED_CODE": "Your verification code has expired", - "SENDING": "Sending...", - "SENT": "Sent!", - "PASSWORD": "Password", - "LINK_PASSWORD": "Enter password to unlock the album", - "RETURN_PASSPHRASE_HINT": "Password", - "SET_PASSPHRASE": "Set password", - "VERIFY_PASSPHRASE": "Sign in", - "INCORRECT_PASSPHRASE": "Incorrect password", - "ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data", - "PASSPHRASE_DISCLAIMER": "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.", + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", "WELCOME_TO_ENTE_HEADING": "به خوش آمدید", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...", - "PASSPHRASE_HINT": "Password", - "CONFIRM_PASSPHRASE": "Confirm password", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", - "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", - "CREATE_COLLECTION": "New album", - "ENTER_ALBUM_NAME": "Album name", - "CLOSE_OPTION": "Close (Esc)", - "ENTER_FILE_NAME": "File name", - "CLOSE": "Close", - "NO": "No", - "NOTHING_HERE": "Nothing to see here yet 👀", - "UPLOAD": "Upload", - "IMPORT": "Import", - "ADD_PHOTOS": "Add photos", - "ADD_MORE_PHOTOS": "Add more photos", - "add_photos_one": "Add 1 item", - "add_photos_other": "Add {{count, number}} items", - "SELECT_PHOTOS": "Select photos", - "FILE_UPLOAD": "File Upload", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", "UPLOAD_STAGE_MESSAGE": { - "0": "Preparing to upload", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", - "5": "Backup complete" + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", - "SUBSCRIPTION_EXPIRED": "Subscription expired", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", - "STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", - "NO_ACCOUNT": "Don't have an account", - "ACCOUNT_EXISTS": "Already have an account", - "CREATE": "Create", - "DOWNLOAD": "Download", - "DOWNLOAD_OPTION": "Download (D)", - "DOWNLOAD_FAVORITES": "Download favorites", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", - "COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", - "ZOOM_IN_OUT": "Zoom in/out", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", - "UPLOAD_FIRST_PHOTO": "Upload your first photo", - "IMPORT_YOUR_FOLDERS": "Import your folders", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", - "TRASH_FILES_TITLE": "Delete files?", - "TRASH_FILE_TITLE": "Delete file?", - "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", - "DELETE": "Delete", - "DELETE_OPTION": "Delete (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album", - "OR": "or", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", - "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", - "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", - "CHANGE_PASSWORD": "Change password", - "GO_BACK": "Go back", - "RECOVERY_KEY": "Recovery key", - "SAVE_LATER": "Do this later", - "SAVE": "Save Key", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", - "FORGOT_PASSWORD": "Forgot password", - "RECOVER_ACCOUNT": "Recover account", - "RECOVERY_KEY_HINT": "Recovery key", - "RECOVER": "Recover", - "NO_RECOVERY_KEY": "No recovery key?", - "INCORRECT_RECOVERY_KEY": "Incorrect recovery key", - "SORRY": "Sorry", - "NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to {{emailID}} from your registered email address", - "CONTACT_SUPPORT": "Contact support", - "REQUEST_FEATURE": "Request Feature", - "SUPPORT": "Support", - "CONFIRM": "Confirm", - "CANCEL": "Cancel", - "LOGOUT": "Logout", - "DELETE_ACCOUNT": "Delete account", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", - "LOGOUT_MESSAGE": "Are you sure you want to logout?", - "CHANGE_EMAIL": "Change email", - "OK": "OK", - "SUCCESS": "Success", - "ERROR": "Error", - "MESSAGE": "Message", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", - "DOWNLOAD_APP": "Download desktop app", - "EXPORT": "Export Data", - "SUBSCRIPTION": "Subscription", - "SUBSCRIBE": "Subscribe", - "MANAGEMENT_PORTAL": "Manage payment method", - "MANAGE_FAMILY_PORTAL": "Manage family", - "LEAVE_FAMILY_PLAN": "Leave family plan", - "LEAVE": "Leave", - "LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?", - "CHOOSE_PLAN": "Choose your plan", - "MANAGE_PLAN": "Manage your subscription", - "ACTIVE": "Active", - "OFFLINE_MSG": "You are offline, cached memories are being shown", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", - "FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by", - "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please upgrade", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", - "SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again", - "SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", - "UPDATE_PAYMENT_METHOD": "Update payment method", - "MONTHLY": "Monthly", - "YEARLY": "Yearly", - "UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?", - "UPDATE_SUBSCRIPTION": "Change plan", - "CANCEL_SUBSCRIPTION": "Cancel subscription", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", - "REACTIVATE_SUBSCRIPTION": "Reactivate subscription", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", - "RENAME": "Rename", - "RENAME_FILE": "Rename file", - "RENAME_COLLECTION": "Rename album", - "DELETE_COLLECTION_TITLE": "Delete album?", - "DELETE_COLLECTION": "Delete album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", - "DELETE_PHOTOS": "Delete photos", - "KEEP_PHOTOS": "Keep photos", - "SHARE": "Share", - "SHARE_COLLECTION": "Share album", - "SHAREES": "Shared with", - "SHARE_WITH_SELF": "Oops, you cannot share with yourself", - "ALREADY_SHARED": "Oops, you're already sharing this with {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", - "DOWNLOAD_COLLECTION": "Download album", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", - "SEARCH": "Search", - "SEARCH_RESULTS": "Search results", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", "SEARCH_TYPE": { - "COLLECTION": "Album", - "LOCATION": "Location", - "CITY": "Location", - "DATE": "Date", - "FILE_NAME": "File name", - "THING": "Content", - "FILE_CAPTION": "Description", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" }, - "photos_count_zero": "No memories", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", - "ADD_TO_COLLECTION": "Add to album", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", - "PEOPLE": "People", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", - "INFO": "Info ", - "INFO_OPTION": "Info (I)", - "FILE_NAME": "File name", - "CAPTION_PLACEHOLDER": "Add a description", - "LOCATION": "Location", - "SHOW_ON_MAP": "View on OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", - "EXIF": "EXIF", - "ISO": "ISO", - "TWO_FACTOR": "Two-factor", - "TWO_FACTOR_AUTHENTICATION": "Two-factor authentication", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", - "ENTER_CODE_MANUALLY": "Enter the code manually", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", - "SCAN_QR_CODE": "Scan QR code instead", - "ENABLE_TWO_FACTOR": "Enable two-factor", - "ENABLE": "Enable", - "LOST_DEVICE": "Lost two-factor device", - "INCORRECT_CODE": "Incorrect code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", - "EXPORT_DATA": "Export data", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", - "SEND_OTT": "Send OTP", - "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", - "FAILED_UPLOADS": "Failed uploads ", - "SKIPPED_FILES": "Ignored uploads", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", - "SKIPPED_VIDEOS": "Skipped videos", - "INPROGRESS_METADATA_EXTRACTION": "In progress", - "INPROGRESS_UPLOADS": "Uploads in progress", - "TOO_LARGE_UPLOADS": "Large files", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan", - "TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", - "ARCHIVE": "Archive", - "FAVORITES": "Favorites", - "ARCHIVE_COLLECTION": "Archive album", - "ARCHIVE_SECTION_NAME": "Archive", - "ALL_SECTION_NAME": "All", - "MOVE_TO_COLLECTION": "Move to album", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", - "MOVE": "Move", - "ADD": "Add", - "REMOVE": "Remove", - "YES_REMOVE": "Yes, remove", - "REMOVE_FROM_COLLECTION": "Remove from album", - "TRASH": "Trash", - "MOVE_TO_TRASH": "Move to trash", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", - "DELETE_PERMANENTLY": "Delete permanently", - "RESTORE": "Restore", - "RESTORE_TO_COLLECTION": "Restore to album", - "EMPTY_TRASH": "Empty trash", - "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", - "LEAVE_SHARED_ALBUM": "Yes, leave", - "LEAVE_ALBUM": "Leave album", - "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", - "SORT_BY_CREATION_TIME_ASCENDING": "Oldest", - "SORT_BY_UPDATION_TIME_DESCENDING": "Last updated", - "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", - "FIX_THUMBNAIL": "Compress", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", - "INSTALL": "Install", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", - "NOT_FOUND": "404 - not found", - "LINK_EXPIRED": "Link expired", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", - "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", - "UPLOAD_DIRS": "Folder", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", - "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", - "ALL_ALBUMS": "All Albums", - "ALBUMS": "Albums", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", - "CREATE_ACCOUNT": "Create account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", - "YOU": "You", - "FAMILY": "Family", - "FREE": "free", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", - "AUTHENTICATE": "Authenticate", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", - "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", - "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", - "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", - "PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong", - "PREFERENCES": "Preferences", - "LANGUAGE": "Language", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" }, "AFTER_TIME": { - "HOUR": "after an hour", - "DAY": "after a day", - "WEEK": "after a week", - "MONTH": "after a month", - "YEAR": "after a year" + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" }, - "COPY_LINK": "Copy link", - "DONE": "Done", - "LINK_SHARE_TITLE": "Or share a link", - "REMOVE_LINK": "Remove link", - "CREATE_PUBLIC_SHARING": "Create public link", - "PUBLIC_LINK_CREATED": "Public link created", - "PUBLIC_LINK_ENABLED": "Public link enabled", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/fi-FI/translation.json b/web/apps/auth/public/locales/fi-FI/translation.json index 6870df319..888ed7093 100644 --- a/web/apps/auth/public/locales/fi-FI/translation.json +++ b/web/apps/auth/public/locales/fi-FI/translation.json @@ -1,644 +1,654 @@ { - "HERO_SLIDE_1_TITLE": "
Private backups
for your memories
", - "HERO_SLIDE_1": "End-to-end encrypted by default", - "HERO_SLIDE_2_TITLE": "
Safely stored
at a fallout shelter
", - "HERO_SLIDE_2": "Designed to outlive", - "HERO_SLIDE_3_TITLE": "
Available
everywhere
", - "HERO_SLIDE_3": "Android, iOS, Web, Desktop", - "LOGIN": "Login", - "SIGN_UP": "Signup", - "NEW_USER": "New to ente", - "EXISTING_USER": "Existing user", - "ENTER_NAME": "Enter name", - "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", - "ENTER_EMAIL": "Enter email address", - "EMAIL_ERROR": "Enter a valid email", - "REQUIRED": "Required", - "EMAIL_SENT": "Verification code sent to {{email}}", - "CHECK_INBOX": "Please check your inbox (and spam) to complete verification", - "ENTER_OTT": "Verification code", - "RESEND_MAIL": "Resend code", - "VERIFY": "Verify", - "UNKNOWN_ERROR": "Something went wrong, please try again", - "INVALID_CODE": "Invalid verification code", - "EXPIRED_CODE": "Your verification code has expired", - "SENDING": "Sending...", - "SENT": "Sent!", - "PASSWORD": "Password", - "LINK_PASSWORD": "Enter password to unlock the album", - "RETURN_PASSPHRASE_HINT": "Password", - "SET_PASSPHRASE": "Set password", - "VERIFY_PASSPHRASE": "Sign in", - "INCORRECT_PASSPHRASE": "Incorrect password", - "ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data", - "PASSPHRASE_DISCLAIMER": "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.", - "WELCOME_TO_ENTE_HEADING": "Welcome to ", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...", - "PASSPHRASE_HINT": "Password", - "CONFIRM_PASSPHRASE": "Confirm password", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", - "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", - "CREATE_COLLECTION": "New album", - "ENTER_ALBUM_NAME": "Album name", - "CLOSE_OPTION": "Close (Esc)", - "ENTER_FILE_NAME": "File name", - "CLOSE": "Close", - "NO": "No", - "NOTHING_HERE": "Nothing to see here yet 👀", - "UPLOAD": "Upload", - "IMPORT": "Import", - "ADD_PHOTOS": "Add photos", - "ADD_MORE_PHOTOS": "Add more photos", - "add_photos_one": "Add 1 item", - "add_photos_other": "Add {{count, number}} items", - "SELECT_PHOTOS": "Select photos", - "FILE_UPLOAD": "File Upload", + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", "UPLOAD_STAGE_MESSAGE": { - "0": "Preparing to upload", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", - "5": "Backup complete" + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", - "SUBSCRIPTION_EXPIRED": "Subscription expired", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", - "STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", - "NO_ACCOUNT": "Don't have an account", - "ACCOUNT_EXISTS": "Already have an account", - "CREATE": "Create", - "DOWNLOAD": "Download", - "DOWNLOAD_OPTION": "Download (D)", - "DOWNLOAD_FAVORITES": "Download favorites", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", - "COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", - "ZOOM_IN_OUT": "Zoom in/out", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", - "UPLOAD_FIRST_PHOTO": "Upload your first photo", - "IMPORT_YOUR_FOLDERS": "Import your folders", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", - "TRASH_FILES_TITLE": "Delete files?", - "TRASH_FILE_TITLE": "Delete file?", - "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", - "DELETE": "Delete", - "DELETE_OPTION": "Delete (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album", - "OR": "or", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", - "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", - "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", - "CHANGE_PASSWORD": "Change password", - "GO_BACK": "Go back", - "RECOVERY_KEY": "Recovery key", - "SAVE_LATER": "Do this later", - "SAVE": "Save Key", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", - "FORGOT_PASSWORD": "Forgot password", - "RECOVER_ACCOUNT": "Recover account", - "RECOVERY_KEY_HINT": "Recovery key", - "RECOVER": "Recover", - "NO_RECOVERY_KEY": "No recovery key?", - "INCORRECT_RECOVERY_KEY": "Incorrect recovery key", - "SORRY": "Sorry", - "NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to {{emailID}} from your registered email address", - "CONTACT_SUPPORT": "Contact support", - "REQUEST_FEATURE": "Request Feature", - "SUPPORT": "Support", - "CONFIRM": "Confirm", - "CANCEL": "Cancel", - "LOGOUT": "Logout", - "DELETE_ACCOUNT": "Delete account", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", - "LOGOUT_MESSAGE": "Are you sure you want to logout?", - "CHANGE_EMAIL": "Change email", - "OK": "OK", - "SUCCESS": "Success", - "ERROR": "Error", - "MESSAGE": "Message", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", - "DOWNLOAD_APP": "Download desktop app", - "EXPORT": "Export Data", - "SUBSCRIPTION": "Subscription", - "SUBSCRIBE": "Subscribe", - "MANAGEMENT_PORTAL": "Manage payment method", - "MANAGE_FAMILY_PORTAL": "Manage family", - "LEAVE_FAMILY_PLAN": "Leave family plan", - "LEAVE": "Leave", - "LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?", - "CHOOSE_PLAN": "Choose your plan", - "MANAGE_PLAN": "Manage your subscription", - "ACTIVE": "Active", - "OFFLINE_MSG": "You are offline, cached memories are being shown", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", - "FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by", - "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please upgrade", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", - "SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again", - "SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", - "UPDATE_PAYMENT_METHOD": "Update payment method", - "MONTHLY": "Monthly", - "YEARLY": "Yearly", - "UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?", - "UPDATE_SUBSCRIPTION": "Change plan", - "CANCEL_SUBSCRIPTION": "Cancel subscription", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", - "REACTIVATE_SUBSCRIPTION": "Reactivate subscription", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", - "RENAME": "Rename", - "RENAME_FILE": "Rename file", - "RENAME_COLLECTION": "Rename album", - "DELETE_COLLECTION_TITLE": "Delete album?", - "DELETE_COLLECTION": "Delete album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", - "DELETE_PHOTOS": "Delete photos", - "KEEP_PHOTOS": "Keep photos", - "SHARE": "Share", - "SHARE_COLLECTION": "Share album", - "SHAREES": "Shared with", - "SHARE_WITH_SELF": "Oops, you cannot share with yourself", - "ALREADY_SHARED": "Oops, you're already sharing this with {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", - "DOWNLOAD_COLLECTION": "Download album", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", - "SEARCH": "Search", - "SEARCH_RESULTS": "Search results", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", "SEARCH_TYPE": { - "COLLECTION": "Album", - "LOCATION": "Location", - "CITY": "Location", - "DATE": "Date", - "FILE_NAME": "File name", - "THING": "Content", - "FILE_CAPTION": "Description", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" }, - "photos_count_zero": "No memories", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", - "ADD_TO_COLLECTION": "Add to album", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", - "PEOPLE": "People", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", - "INFO": "Info ", - "INFO_OPTION": "Info (I)", - "FILE_NAME": "File name", - "CAPTION_PLACEHOLDER": "Add a description", - "LOCATION": "Location", - "SHOW_ON_MAP": "View on OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", - "EXIF": "EXIF", - "ISO": "ISO", - "TWO_FACTOR": "Two-factor", - "TWO_FACTOR_AUTHENTICATION": "Two-factor authentication", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", - "ENTER_CODE_MANUALLY": "Enter the code manually", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", - "SCAN_QR_CODE": "Scan QR code instead", - "ENABLE_TWO_FACTOR": "Enable two-factor", - "ENABLE": "Enable", - "LOST_DEVICE": "Lost two-factor device", - "INCORRECT_CODE": "Incorrect code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", - "EXPORT_DATA": "Export data", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", - "SEND_OTT": "Send OTP", - "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", - "FAILED_UPLOADS": "Failed uploads ", - "SKIPPED_FILES": "Ignored uploads", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", - "SKIPPED_VIDEOS": "Skipped videos", - "INPROGRESS_METADATA_EXTRACTION": "In progress", - "INPROGRESS_UPLOADS": "Uploads in progress", - "TOO_LARGE_UPLOADS": "Large files", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan", - "TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", - "ARCHIVE": "Archive", - "FAVORITES": "Favorites", - "ARCHIVE_COLLECTION": "Archive album", - "ARCHIVE_SECTION_NAME": "Archive", - "ALL_SECTION_NAME": "All", - "MOVE_TO_COLLECTION": "Move to album", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", - "MOVE": "Move", - "ADD": "Add", - "REMOVE": "Remove", - "YES_REMOVE": "Yes, remove", - "REMOVE_FROM_COLLECTION": "Remove from album", - "TRASH": "Trash", - "MOVE_TO_TRASH": "Move to trash", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", - "DELETE_PERMANENTLY": "Delete permanently", - "RESTORE": "Restore", - "RESTORE_TO_COLLECTION": "Restore to album", - "EMPTY_TRASH": "Empty trash", - "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", - "LEAVE_SHARED_ALBUM": "Yes, leave", - "LEAVE_ALBUM": "Leave album", - "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", - "SORT_BY_CREATION_TIME_ASCENDING": "Oldest", - "SORT_BY_UPDATION_TIME_DESCENDING": "Last updated", - "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", - "FIX_THUMBNAIL": "Compress", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", - "INSTALL": "Install", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", - "NOT_FOUND": "404 - not found", - "LINK_EXPIRED": "Link expired", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", - "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", - "UPLOAD_DIRS": "Folder", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", - "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", - "ALL_ALBUMS": "All Albums", - "ALBUMS": "Albums", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", - "CREATE_ACCOUNT": "Create account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", - "YOU": "You", - "FAMILY": "Family", - "FREE": "free", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", - "AUTHENTICATE": "Authenticate", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", - "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", - "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", - "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", - "PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong", - "PREFERENCES": "Preferences", - "LANGUAGE": "Language", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" }, "AFTER_TIME": { - "HOUR": "after an hour", - "DAY": "after a day", - "WEEK": "after a week", - "MONTH": "after a month", - "YEAR": "after a year" + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" }, - "COPY_LINK": "Copy link", - "DONE": "Done", - "LINK_SHARE_TITLE": "Or share a link", - "REMOVE_LINK": "Remove link", - "CREATE_PUBLIC_SHARING": "Create public link", - "PUBLIC_LINK_CREATED": "Public link created", - "PUBLIC_LINK_ENABLED": "Public link enabled", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/fr-FR/translation.json b/web/apps/auth/public/locales/fr-FR/translation.json index b94bc3973..43d959069 100644 --- a/web/apps/auth/public/locales/fr-FR/translation.json +++ b/web/apps/auth/public/locales/fr-FR/translation.json @@ -38,11 +38,9 @@ "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Génération des clés de chiffrement...", "PASSPHRASE_HINT": "Mot de passe", "CONFIRM_PASSPHRASE": "Confirmer le mot de passe", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", + "REFERRAL_CODE_HINT": "Comment avez-vous entendu parler de Ente? (facultatif)", + "REFERRAL_INFO": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", "PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Ceci est une fonction de navigateur dédiée aux développeurs. Veuillez ne pas copier-coller un code non vérifié à cet endroit.", "CREATE_COLLECTION": "Nouvel album", "ENTER_ALBUM_NAME": "Nom de l'album", "CLOSE_OPTION": "Fermer (Échap)", @@ -159,7 +157,7 @@ "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renouveler le {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Pris fin le {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Votre abonnement sera annulé le {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Votre module {{storage, string}} est valable jusqu'au {{date, dateTime}}", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Vous avez dépassé votre quota de stockage, veuillez mettre à niveau ", "SUBSCRIPTION_PURCHASE_SUCCESS": "

Nous avons reçu votre paiement

Votre abonnement est valide jusqu'au {{date, dateTime}}

", "SUBSCRIPTION_PURCHASE_CANCELLED": "Votre achat est annulé, veuillez réessayer si vous souhaitez vous abonner", @@ -174,7 +172,7 @@ "UPDATE_SUBSCRIPTION": "Changer de plan", "CANCEL_SUBSCRIPTION": "Annuler l'abonnement", "CANCEL_SUBSCRIPTION_MESSAGE": "

Toutes vos données seront supprimées de nos serveurs à la fin de cette période d'abonnement.

Voulez-vous vraiment annuler votre abonnement?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "Êtes-vous sûr de vouloir annuler votre abonnement ", "SUBSCRIPTION_CANCEL_FAILED": "Échec lors de l'annulation de l'abonnement", "SUBSCRIPTION_CANCEL_SUCCESS": "Votre abonnement a bien été annulé", "REACTIVATE_SUBSCRIPTION": "Réactiver l'abonnement", @@ -210,7 +208,7 @@ "SEARCH_TYPE": { "COLLECTION": "l'album", "LOCATION": "Emplacement", - "CITY": "Location", + "CITY": "Adresse", "DATE": "Date", "FILE_NAME": "Nom de fichier", "THING": "Chose", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Téléchargement de {{name}}", "DOWNLOAD_FAILED": "Échec du téléchargement", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} fichiers", - "CRASH_REPORTING": "Rapport de plantage", "CHRISTMAS": "Noël", "CHRISTMAS_EVE": "Réveillon de Noël", "NEW_YEAR": "Nouvel an", @@ -623,22 +620,35 @@ "PHOTO_EDITOR": "Éditeur de photos", "FASTER_UPLOAD": "Chargements plus rapides", "FASTER_UPLOAD_DESCRIPTION": "Router les chargements vers les serveurs à proximité", - "MAGIC_SEARCH_STATUS": "Magic Search Status", + "MAGIC_SEARCH_STATUS": "Statut de la recherche magique", "INDEXED_ITEMS": "Éléments indexés", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "CAST_ALBUM_TO_TV": "Jouer l'album sur la TV", + "ENTER_CAST_PIN_CODE": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.", + "PAIR_DEVICE_TO_TV": "Associer les appareils", + "TV_NOT_FOUND": "TV introuvable. Avez-vous entré le code PIN correctement ?", + "AUTO_CAST_PAIR": "Paire automatique", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "La paire automatique nécessite la connexion aux serveurs Google et ne fonctionne qu'avec les appareils pris en charge par Chromecast. Google ne recevra pas de données sensibles, telles que vos photos.", + "PAIR_WITH_PIN": "Associer avec le code PIN", + "CHOOSE_DEVICE_FROM_BROWSER": "Choisissez un périphérique compatible avec la caste à partir de la fenêtre pop-up du navigateur.", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "L'association avec le code PIN fonctionne pour tout appareil grand écran sur lequel vous voulez lire votre album.", + "VISIT_CAST_ENTE_IO": "Visitez cast.ente.io sur l'appareil que vous voulez associer.", + "CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.", + "CACHE_DIRECTORY": "Dossier du cache", + "FREEHAND": "Main levée", + "APPLY_CROP": "Appliquer le recadrage", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.", + "PASSKEYS": "Clés d'accès", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/it-IT/translation.json b/web/apps/auth/public/locales/it-IT/translation.json index 679478f70..ae450e5fe 100644 --- a/web/apps/auth/public/locales/it-IT/translation.json +++ b/web/apps/auth/public/locales/it-IT/translation.json @@ -39,10 +39,8 @@ "PASSPHRASE_HINT": "Password", "CONFIRM_PASSPHRASE": "Conferma la password", "REFERRAL_CODE_HINT": "Come hai conosciuto Ente? (opzionale)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", + "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Le password non corrispondono", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Questa è una funzionalità del browser destinata agli sviluppatori. Non copiare né incollare codice non verificato qui.", "CREATE_COLLECTION": "Nuovo album", "ENTER_ALBUM_NAME": "Nome album", "CLOSE_OPTION": "Chiudi (Esc)", @@ -85,9 +83,9 @@ "ZOOM_IN_OUT": "Zoom in/out", "PREVIOUS": "Precedente (←)", "NEXT": "Successivo (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", "UPLOAD_FIRST_PHOTO": "Carica la tua prima foto", "IMPORT_YOUR_FOLDERS": "Importa una cartella", "UPLOAD_DROPZONE_MESSAGE": "Rilascia per eseguire il backup dei file", @@ -159,7 +157,7 @@ "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Si rinnova il {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Termina il {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Il tuo abbonamento verrà annullato il {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Hai superato la quota di archiviazione assegnata, si prega di aggiornare ", "SUBSCRIPTION_PURCHASE_SUCCESS": "

Abbiamo ricevuto il tuo pagamento

Il tuo abbonamento è valido fino a {{date, dateTime}}

", "SUBSCRIPTION_PURCHASE_CANCELLED": "Il tuo acquisto è stato annullato, riprova se vuoi iscriverti", @@ -174,23 +172,23 @@ "UPDATE_SUBSCRIPTION": "Cambia piano", "CANCEL_SUBSCRIPTION": "Annulla abbonamento", "CANCEL_SUBSCRIPTION_MESSAGE": "

Tutti i tuoi dati saranno cancellati dai nostri server alla fine di questo periodo di fatturazione.

Sei sicuro di voler annullare il tuo abbonamento?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", "SUBSCRIPTION_CANCEL_FAILED": "Impossibile annullare l'abbonamento", "SUBSCRIPTION_CANCEL_SUCCESS": "Abbonamento annullato con successo", "REACTIVATE_SUBSCRIPTION": "Riattiva abbonamento", "REACTIVATE_SUBSCRIPTION_MESSAGE": "Una volta riattivato, ti verrà addebitato il valore di {{date, dateTime}}", "SUBSCRIPTION_ACTIVATE_SUCCESS": "Iscrizione attivata con successo ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", + "SUBSCRIPTION_ACTIVATE_FAILED": "", "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Grazie", "CANCEL_SUBSCRIPTION_ON_MOBILE": "Annulla abbonamento mobile", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", "MAIL_TO_MANAGE_SUBSCRIPTION": "Per favore contattaci su {{emailID}} per gestire il tuo abbonamento", "RENAME": "Rinomina", "RENAME_FILE": "Rinomina file", "RENAME_COLLECTION": "Rinomina album", "DELETE_COLLECTION_TITLE": "Eliminare l'album?", "DELETE_COLLECTION": "Elimina album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", + "DELETE_COLLECTION_MESSAGE": "", "DELETE_PHOTOS": "Elimina foto", "KEEP_PHOTOS": "Mantieni foto", "SHARE": "Condividi", @@ -205,8 +203,8 @@ "CREATE_ALBUM_FAILED": "Operazione di creazione dell'album fallita, per favore riprova", "SEARCH": "Ricerca", "SEARCH_RESULTS": "Risultati della ricerca", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "NO_RESULTS": "", + "SEARCH_HINT": "", "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Posizione", @@ -216,25 +214,25 @@ "THING": "Contenuto", "FILE_CAPTION": "Descrizione", "FILE_TYPE": "Tipo del file", - "CLIP": "Magic" + "CLIP": "" }, "photos_count_zero": "Nessuna memoria", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", "ADD_TO_COLLECTION": "Aggiungi all'album", - "SELECTED": "selected", + "SELECTED": "", "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Questo video non può essere riprodotto nel tuo browser", "PEOPLE": "Persone", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", "UNIDENTIFIED_FACES": "volti non identificati", - "OBJECTS": "objects", + "OBJECTS": "", "TEXT": "testo", "INFO": "Info ", - "INFO_OPTION": "Info (I)", + "INFO_OPTION": "", "FILE_NAME": "Nome file", "CAPTION_PLACEHOLDER": "Aggiungi una descrizione", "LOCATION": "Posizione", @@ -244,12 +242,12 @@ "ENABLE_MAPS": "Attivare Mappa?", "ENABLE_MAP": "Attivare mappa", "DISABLE_MAPS": "Disattivare Mappa?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", "EXIF": "EXIF", "ISO": "ISO", "TWO_FACTOR": "Due fattori", @@ -260,41 +258,41 @@ "SCAN_QR_CODE": "Oppure scansiona il codice QR", "ENABLE_TWO_FACTOR": "Attiva due fattori", "ENABLE": "Attiva", - "LOST_DEVICE": "Lost two-factor device", + "LOST_DEVICE": "", "INCORRECT_CODE": "Codice errato", "TWO_FACTOR_INFO": "Aggiungi un ulteriore livello di sicurezza richiedendo più informazioni rispetto a email e password per eseguire l'accesso al tuo account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", "EXPORT_DATA": "Esporta dati", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", "SEND_OTT": "Invia OTP", "EMAIl_ALREADY_OWNED": "Email già in uso", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", "FAILED_UPLOADS": "Caricamento fallito ", "SKIPPED_FILES": "Ignora caricamenti", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", "SUCCESSFUL_UPLOADS": "Caricamenti eseguiti con successo", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", "SKIPPED_VIDEOS": "Video saltati", "INPROGRESS_METADATA_EXTRACTION": "In corso", "INPROGRESS_UPLOADS": "Caricamenti in corso", @@ -302,9 +300,9 @@ "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Spazio insufficiente", "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Questi file non sono stati caricati perché supererebbero la capacità massima del tuo piano di spazio d'archiviazione", "TOO_LARGE_INFO": "Questi file non sono stati caricati perché superano il nostro limite di pesantezza di un file", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", "ARCHIVE": "Archivio", "FAVORITES": "Preferiti", "ARCHIVE_COLLECTION": "Album archiviato", @@ -333,10 +331,10 @@ "LEAVE_SHARED_ALBUM": "Sì, esci", "LEAVE_ALBUM": "Abbandona l'album", "LEAVE_SHARED_ALBUM_TITLE": "Abbandonare l'album condiviso?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", "SORT_BY_CREATION_TIME_ASCENDING": "Meno recente", "SORT_BY_UPDATION_TIME_DESCENDING": "Ultimo aggiornamento", "SORT_BY_NAME": "Nome", @@ -344,180 +342,180 @@ "THUMBNAIL_REPLACED": "Miniature compresse", "FIX_THUMBNAIL": "Comprimi", "FIX_THUMBNAIL_LATER": "Comprimi più tardi", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", "INSTALL": "Installa", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", "participants_zero": "Nessun partecipante", "participants_one": "1 partecipante", "participants_other": "{{count, number}} partecipanti", - "ADD_VIEWERS": "Add viewers", + "ADD_VIEWERS": "", "PARTICIPANTS": "Partecipanti", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", "REMOVE_PARTICIPANT": "Rimuovere?", "CONFIRM_REMOVE": "Sì, rimuovi", "MANAGE": "Gestisci", "ADDED_AS": "Aggiunto come", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", + "COLLABORATOR_RIGHTS": "", "REMOVE_PARTICIPANT_HEAD": "Rimuovi partecipante", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - non trovato", "LINK_EXPIRED": "Link scaduto", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", "UPLOAD_DIRS": "Cartella", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", "albums_one": "1 Album", "albums_other": "{{count, number}} Album", "ALL_ALBUMS": "Tutti gli Album", "ALBUMS": "Album", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "Crea account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", "YOU": "Tu", "FAMILY": "Famiglia", "FREE": "gratis", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", + "OF": "", + "WATCHED_FOLDERS": "", "NO_FOLDERS_ADDED": "Ancora nessuna cartella aggiunta!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "Cambia Cartella", "TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali", "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", "FREE_PLAN_DESCRIPTION": "1 GB per 1 anno", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", "AUTHENTICATE": "Autenticati", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", + "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": "Oggi", "YESTERDAY": "Ieri", "NAME_PLACEHOLDER": "Nome...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", "CHOSE_THEME": "Seleziona tema", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", "ML_MORE_DETAILS": "Più dettagli", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", "ADVANCED": "Avanzate", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", "PASSPHRASE_STRENGTH_WEAK": "Sicurezza password: Debole", "PASSPHRASE_STRENGTH_MODERATE": "Sicurezza password: Moderata", "PASSPHRASE_STRENGTH_STRONG": "Sicurezza password: Forte", - "PREFERENCES": "Preferences", + "PREFERENCES": "", "LANGUAGE": "Lingua", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { "B": "B", "KB": "KB", @@ -539,106 +537,118 @@ "CREATE_PUBLIC_SHARING": "Crea link pubblico", "PUBLIC_LINK_CREATED": "Link pubblick creato", "PUBLIC_LINK_ENABLED": "Link pubblico attivato", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Seleziona un motivo", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/ko-KR/translation.json b/web/apps/auth/public/locales/ko-KR/translation.json new file mode 100644 index 000000000..4fbe6c077 --- /dev/null +++ b/web/apps/auth/public/locales/ko-KR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요", + "HERO_SLIDE_1": "종단간 암호화가 기본지원입니다", + "HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다", + "HERO_SLIDE_2": "오랫동안 보존할 수 있도록한 설계", + "HERO_SLIDE_3_TITLE": "
어디에서나
이용가능
", + "HERO_SLIDE_3": "안드로이드, iOS, 웹, 데스크탑", + "LOGIN": "로그인", + "SIGN_UP": "회원가입", + "NEW_USER": "ente의 새소식", + "EXISTING_USER": "기존 사용자", + "ENTER_NAME": "이름 입력", + "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", + "ENTER_EMAIL": "이메일 주소를 입력하세요", + "EMAIL_ERROR": "올바른 이메일을 입력하세요", + "REQUIRED": "필수", + "EMAIL_SENT": "{{email}} 로 인증 코드가 전송되었습니다", + "CHECK_INBOX": "인증을 완료하기 위해 당신의 메일 수신함(그리고 스팸 수신함)을 확인하세요.", + "ENTER_OTT": "인증 코드", + "RESEND_MAIL": "코드 재전송하기", + "VERIFY": "인증", + "UNKNOWN_ERROR": "문제가 생긴 것 같아요. 다시 시도하세요", + "INVALID_CODE": "잘못된 인증 코드", + "EXPIRED_CODE": "입력한 인증 코드가 만료되었습니다", + "SENDING": "전송 중...", + "SENT": "발송 완료!", + "PASSWORD": "비밀번호", + "LINK_PASSWORD": "앨범 잠금해제를 위해 비밀번호를 입력하세요", + "RETURN_PASSPHRASE_HINT": "비밀번호", + "SET_PASSPHRASE": "비밀번호 설정", + "VERIFY_PASSPHRASE": "로그인", + "INCORRECT_PASSPHRASE": "잘못된 비밀번호입니다", + "ENTER_ENC_PASSPHRASE": "당신의 데이터를 암호화하는 데 사용할 수 있는 비밀번호를 입력하세요", + "PASSPHRASE_DISCLAIMER": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키 없다면 데이터 복구를 도와드릴 수 없습니다.", + "WELCOME_TO_ENTE_HEADING": "환영합니다 ", + "WELCOME_TO_ENTE_SUBHEADING": "End-to-End 암호화된 사진 저장 및 공유", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "당신 최고의 사진이 있는 곳", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "암호 키 생성 중...", + "PASSPHRASE_HINT": "비밀번호", + "CONFIRM_PASSPHRASE": "비밀번호 확인", + "REFERRAL_CODE_HINT": "어떻게 Ente에 대해 들으셨나요? (선택사항)", + "REFERRAL_INFO": "우리는 앱 설치를 추적하지 않습니다. 우리를 알게 된 곳을 남겨주시면 우리에게 도움이 될꺼에요!", + "PASSPHRASE_MATCH_ERROR": "비밀번호가 일치하지 않습니다", + "CREATE_COLLECTION": "새 앨범", + "ENTER_ALBUM_NAME": "앨범 이름", + "CLOSE_OPTION": "닫기 (Esc)", + "ENTER_FILE_NAME": "파일 이름", + "CLOSE": "닫기", + "NO": "아니오", + "NOTHING_HERE": "아직 볼 수 있는 것이 없어요 👀", + "UPLOAD": "업로드", + "IMPORT": "가져오기", + "ADD_PHOTOS": "사진 추가", + "ADD_MORE_PHOTOS": "사진 더 추가하기", + "add_photos_one": "아이템 하나 추가", + "add_photos_other": "아이템 {{count, number}} 개 추가하기", + "SELECT_PHOTOS": "사진 선택하기", + "FILE_UPLOAD": "파일 업로드", + "UPLOAD_STAGE_MESSAGE": { + "0": "업로드 준비중", + "1": "구글 메타데이타 파일들 읽는중", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일 메타데이터가 추출되었습니다", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일이 처리되었습니다", + "4": "남은 업로드 취소중", + "5": "백업 완료" + }, + "FILE_NOT_UPLOADED_LIST": "아래 파일들은 업로드 되지 않았습니다", + "SUBSCRIPTION_EXPIRED": "구독 만료", + "SUBSCRIPTION_EXPIRED_MESSAGE": "당신 구독이 만료되었으니, 구독을 갱신해주세요", + "STORAGE_QUOTA_EXCEEDED": "스토리지 제한이 초과되었습니다", + "INITIAL_LOAD_DELAY_WARNING": "처음 로딩시 다소 시간이 걸릴 수 있습니다", + "USER_DOES_NOT_EXIST": "죄송합니다. 해당 이메일을 사용하는 사용자를 찾을 수 없습니다", + "NO_ACCOUNT": "계정이 없습니다", + "ACCOUNT_EXISTS": "이미 계정이 있습니다", + "CREATE": "만들기", + "DOWNLOAD": "다운로드", + "DOWNLOAD_OPTION": "다운로드 (D)", + "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/auth/public/locales/nl-NL/translation.json b/web/apps/auth/public/locales/nl-NL/translation.json index 1bfeb6d25..15d9bfdba 100644 --- a/web/apps/auth/public/locales/nl-NL/translation.json +++ b/web/apps/auth/public/locales/nl-NL/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)", "REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", "PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Dit is een browserfunctie bedoeld voor ontwikkelaars. Gelieve hier geen niet-geverifieerde code te kopiëren/plakken.", "CREATE_COLLECTION": "Nieuw album", "ENTER_ALBUM_NAME": "Album naam", "CLOSE_OPTION": "Sluiten (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "{{name}} downloaden", "DOWNLOAD_FAILED": "Download mislukt", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} bestanden", - "CRASH_REPORTING": "Foutenrapportering", "CHRISTMAS": "Kerst", "CHRISTMAS_EVE": "Kerstavond", "NEW_YEAR": "Nieuwjaar", @@ -625,20 +622,33 @@ "FASTER_UPLOAD_DESCRIPTION": "Uploaden door nabije servers", "MAGIC_SEARCH_STATUS": "Magische Zoekfunctie Status", "INDEXED_ITEMS": "Geïndexeerde bestanden", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "Cache map", - "PASSKEYS": "Passkeys", "FREEHAND": "Losse hand", "APPLY_CROP": "Bijsnijden toepassen", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/pt-BR/translation.json b/web/apps/auth/public/locales/pt-BR/translation.json index dd264db38..0da001742 100644 --- a/web/apps/auth/public/locales/pt-BR/translation.json +++ b/web/apps/auth/public/locales/pt-BR/translation.json @@ -8,7 +8,7 @@ "LOGIN": "Entrar", "SIGN_UP": "Registrar", "NEW_USER": "Novo no ente", - "EXISTING_USER": "Utilizador existente", + "EXISTING_USER": "Usuário existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", "ENTER_EMAIL": "Insira o endereço de e-mail", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Como você ouviu sobre o Ente? (opcional)", "REFERRAL_INFO": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "PASSPHRASE_MATCH_ERROR": "As senhas não coincidem", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "Este é um recurso de navegador destinado a desenvolvedores. Por favor, não copie e cole o código não confirmado aqui.", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", @@ -229,7 +227,7 @@ "INDEXING_SCHEDULED": "Indexação está programada...", "ANALYZING_PHOTOS": "Indexando fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", "INDEXING_PEOPLE": "Indexando pessoas em {{indexStatus.nSyncedFiles,number}} fotos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", + "INDEXING_DONE": "Foram indexadas {{indexStatus.nSyncedFiles,number}} fotos", "UNIDENTIFIED_FACES": "rostos não identificados", "OBJECTS": "objetos", "TEXT": "texto", @@ -349,15 +347,15 @@ "REPLACE_THUMBNAIL_NOOP": "Você não tem nenhuma miniatura que possa ser compactadas mais", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Não foi possível compactar algumas das suas miniaturas, por favor tente novamente", "FIX_CREATION_TIME": "Corrigir hora", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", + "FIX_CREATION_TIME_IN_PROGRESS": "Corrigindo horário", + "CREATION_TIME_UPDATED": "Hora do arquivo atualizado", "UPDATE_CREATION_TIME_NOT_STARTED": "Selecione a carteira que você deseja usar", "UPDATE_CREATION_TIME_COMPLETED": "Todos os arquivos atualizados com sucesso", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "A atualização do horário falhou para alguns arquivos, por favor, tente novamente", "CAPTION_CHARACTER_LIMIT": "5000 caracteres no máximo", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", + "DATE_TIME_ORIGINAL": "Data e Hora Original", + "DATE_TIME_DIGITIZED": "Data e Hora Digitalizada", + "METADATA_DATE": "Data de Metadados", "CUSTOM_TIME": "Tempo personalizado", "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planos", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Falha ao abrir planos", @@ -410,7 +408,7 @@ "SHARED_USING": "Compartilhar usando ", "ENTE_IO": "ente.io", "SHARING_REFERRAL_CODE": "Use o código {{referralCode}} para obter 10 GB de graça", - "LIVE": "LIVE", + "LIVE": "AO VIVO", "DISABLE_PASSWORD": "Desativar bloqueio por senha", "DISABLE_PASSWORD_MESSAGE": "Tem certeza que deseja desativar o bloqueio por senha?", "PASSWORD_LOCK": "Bloqueio de senha", @@ -508,8 +506,8 @@ "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente irá parar de processar geometria facial.

Você pode reativar o reconhecimento facial novamente, se desejar, então esta operação está segura.

", "ADVANCED": "Avançado", "FACE_SEARCH_CONFIRMATION": "Eu entendo, e desejo permitir que o ente processe a geometria do rosto", - "LABS": "Labs", - "YOURS": "yours", + "LABS": "Laboratórios", + "YOURS": "seu", "PASSPHRASE_STRENGTH_WEAK": "Força da senha: fraca", "PASSPHRASE_STRENGTH_MODERATE": "Força da senha: moderada", "PASSPHRASE_STRENGTH_STRONG": "Força da senha: forte", @@ -572,7 +570,7 @@ "FEEDBACK_REQUIRED": "Por favor, ajude-nos com esta informação", "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "O que o outro serviço faz melhor?", "RECOVER_TWO_FACTOR": "Recuperar dois fatores", - "at": "at", + "at": "em", "AUTH_NEXT": "próximo", "AUTH_DOWNLOAD_MOBILE_APP": "Baixe nosso aplicativo móvel para gerenciar seus segredos", "HIDDEN": "Escondido", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Transferindo {{name}}", "DOWNLOAD_FAILED": "Falha ao baixar", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", - "CRASH_REPORTING": "Relatório de falhas", "CHRISTMAS": "Natal", "CHRISTMAS_EVE": "Véspera de Natal", "NEW_YEAR": "Ano Novo", @@ -607,7 +604,7 @@ "BLUR": "Desfoque", "INVERT_COLORS": "Inverter Cores", "ASPECT_RATIO": "Proporção da imagem", - "SQUARE": "Square", + "SQUARE": "Quadrado", "ROTATE_LEFT": "Girar para a Esquerda", "ROTATE_RIGHT": "Girar para a Direita", "FLIP_VERTICALLY": "Inverter verticalmente", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Acesse cast.ente.io no dispositivo que você deseja parear.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.", "CACHE_DIRECTORY": "Pasta de Cache", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", + "FREEHAND": "Mão livre", "APPLY_CROP": "Aplicar Recorte", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", + "PASSKEYS": "Chaves de acesso", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/pt-PT/translation.json b/web/apps/auth/public/locales/pt-PT/translation.json index ffb1debb4..230980326 100644 --- a/web/apps/auth/public/locales/pt-PT/translation.json +++ b/web/apps/auth/public/locales/pt-PT/translation.json @@ -1,8 +1,8 @@ { "HERO_SLIDE_1_TITLE": "
Backups privados
para as suas memórias
", - "HERO_SLIDE_1": "End-to-end encrypted by default", - "HERO_SLIDE_2_TITLE": "
Safely stored
at a fallout shelter
", - "HERO_SLIDE_2": "Designed to outlive", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", "HERO_SLIDE_3_TITLE": "
Disponível
em qualquer lugar
", "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Entrar", @@ -30,27 +30,25 @@ "SET_PASSPHRASE": "Definir palavra-passe", "VERIFY_PASSPHRASE": "Entrar", "INCORRECT_PASSPHRASE": "Palavra-passe incorreta", - "ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data", - "PASSPHRASE_DISCLAIMER": "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.", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", "WELCOME_TO_ENTE_HEADING": "Bem-vindo ao ", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...", - "PASSPHRASE_HINT": "Password", - "CONFIRM_PASSPHRASE": "Confirm password", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", - "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", "ENTER_FILE_NAME": "Nome do ficheiro", "CLOSE": "Fechar", "NO": "Não", - "NOTHING_HERE": "Nothing to see here yet 👀", - "UPLOAD": "Upload", + "NOTHING_HERE": "", + "UPLOAD": "", "IMPORT": "Importar", "ADD_PHOTOS": "Adicionar fotos", "ADD_MORE_PHOTOS": "Adicionar mais fotos", @@ -59,586 +57,598 @@ "SELECT_PHOTOS": "Selecionar fotos", "FILE_UPLOAD": "Enviar Ficheiro", "UPLOAD_STAGE_MESSAGE": { - "0": "Preparing to upload", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", - "5": "Backup complete" + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", - "SUBSCRIPTION_EXPIRED": "Subscription expired", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", - "STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", "NO_ACCOUNT": "Não possui uma conta", "ACCOUNT_EXISTS": "Já possui uma conta", "CREATE": "Criar", - "DOWNLOAD": "Download", - "DOWNLOAD_OPTION": "Download (D)", - "DOWNLOAD_FAVORITES": "Download favorites", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", - "COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", - "ZOOM_IN_OUT": "Zoom in/out", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", - "UPLOAD_FIRST_PHOTO": "Upload your first photo", - "IMPORT_YOUR_FOLDERS": "Import your folders", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", - "TRASH_FILES_TITLE": "Delete files?", - "TRASH_FILE_TITLE": "Delete file?", - "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", - "DELETE": "Delete", - "DELETE_OPTION": "Delete (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album", - "OR": "or", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", - "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", - "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", - "CHANGE_PASSWORD": "Change password", - "GO_BACK": "Go back", - "RECOVERY_KEY": "Recovery key", - "SAVE_LATER": "Do this later", - "SAVE": "Save Key", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", - "FORGOT_PASSWORD": "Forgot password", - "RECOVER_ACCOUNT": "Recover account", - "RECOVERY_KEY_HINT": "Recovery key", - "RECOVER": "Recover", - "NO_RECOVERY_KEY": "No recovery key?", - "INCORRECT_RECOVERY_KEY": "Incorrect recovery key", - "SORRY": "Sorry", - "NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to {{emailID}} from your registered email address", - "CONTACT_SUPPORT": "Contact support", - "REQUEST_FEATURE": "Request Feature", - "SUPPORT": "Support", - "CONFIRM": "Confirm", - "CANCEL": "Cancel", - "LOGOUT": "Logout", - "DELETE_ACCOUNT": "Delete account", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", - "LOGOUT_MESSAGE": "Are you sure you want to logout?", - "CHANGE_EMAIL": "Change email", - "OK": "OK", - "SUCCESS": "Success", - "ERROR": "Error", - "MESSAGE": "Message", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", - "DOWNLOAD_APP": "Download desktop app", - "EXPORT": "Export Data", - "SUBSCRIPTION": "Subscription", - "SUBSCRIBE": "Subscribe", - "MANAGEMENT_PORTAL": "Manage payment method", - "MANAGE_FAMILY_PORTAL": "Manage family", - "LEAVE_FAMILY_PLAN": "Leave family plan", - "LEAVE": "Leave", - "LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?", - "CHOOSE_PLAN": "Choose your plan", - "MANAGE_PLAN": "Manage your subscription", - "ACTIVE": "Active", - "OFFLINE_MSG": "You are offline, cached memories are being shown", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", - "FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by", - "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please upgrade", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", - "SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again", - "SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", - "UPDATE_PAYMENT_METHOD": "Update payment method", - "MONTHLY": "Monthly", - "YEARLY": "Yearly", - "UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?", - "UPDATE_SUBSCRIPTION": "Change plan", - "CANCEL_SUBSCRIPTION": "Cancel subscription", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", - "REACTIVATE_SUBSCRIPTION": "Reactivate subscription", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", - "RENAME": "Rename", - "RENAME_FILE": "Rename file", - "RENAME_COLLECTION": "Rename album", - "DELETE_COLLECTION_TITLE": "Delete album?", - "DELETE_COLLECTION": "Delete album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", - "DELETE_PHOTOS": "Delete photos", - "KEEP_PHOTOS": "Keep photos", - "SHARE": "Share", - "SHARE_COLLECTION": "Share album", - "SHAREES": "Shared with", - "SHARE_WITH_SELF": "Oops, you cannot share with yourself", - "ALREADY_SHARED": "Oops, you're already sharing this with {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", - "DOWNLOAD_COLLECTION": "Download album", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", - "SEARCH": "Search", - "SEARCH_RESULTS": "Search results", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", "SEARCH_TYPE": { - "COLLECTION": "Album", - "LOCATION": "Location", - "CITY": "Location", - "DATE": "Date", - "FILE_NAME": "File name", - "THING": "Content", - "FILE_CAPTION": "Description", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" }, - "photos_count_zero": "No memories", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", - "ADD_TO_COLLECTION": "Add to album", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", - "PEOPLE": "People", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", - "INFO": "Info ", - "INFO_OPTION": "Info (I)", - "FILE_NAME": "File name", - "CAPTION_PLACEHOLDER": "Add a description", - "LOCATION": "Location", - "SHOW_ON_MAP": "View on OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", - "EXIF": "EXIF", - "ISO": "ISO", - "TWO_FACTOR": "Two-factor", - "TWO_FACTOR_AUTHENTICATION": "Two-factor authentication", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", - "ENTER_CODE_MANUALLY": "Enter the code manually", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", - "SCAN_QR_CODE": "Scan QR code instead", - "ENABLE_TWO_FACTOR": "Enable two-factor", - "ENABLE": "Enable", - "LOST_DEVICE": "Lost two-factor device", - "INCORRECT_CODE": "Incorrect code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", - "EXPORT_DATA": "Export data", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", - "SEND_OTT": "Send OTP", - "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", - "FAILED_UPLOADS": "Failed uploads ", - "SKIPPED_FILES": "Ignored uploads", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", - "SKIPPED_VIDEOS": "Skipped videos", - "INPROGRESS_METADATA_EXTRACTION": "In progress", - "INPROGRESS_UPLOADS": "Uploads in progress", - "TOO_LARGE_UPLOADS": "Large files", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan", - "TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", - "ARCHIVE": "Archive", - "FAVORITES": "Favorites", - "ARCHIVE_COLLECTION": "Archive album", - "ARCHIVE_SECTION_NAME": "Archive", - "ALL_SECTION_NAME": "All", - "MOVE_TO_COLLECTION": "Move to album", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", - "MOVE": "Move", - "ADD": "Add", - "REMOVE": "Remove", - "YES_REMOVE": "Yes, remove", - "REMOVE_FROM_COLLECTION": "Remove from album", - "TRASH": "Trash", - "MOVE_TO_TRASH": "Move to trash", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", - "DELETE_PERMANENTLY": "Delete permanently", - "RESTORE": "Restore", - "RESTORE_TO_COLLECTION": "Restore to album", - "EMPTY_TRASH": "Empty trash", - "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", - "LEAVE_SHARED_ALBUM": "Yes, leave", - "LEAVE_ALBUM": "Leave album", - "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", - "SORT_BY_CREATION_TIME_ASCENDING": "Oldest", - "SORT_BY_UPDATION_TIME_DESCENDING": "Last updated", - "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", - "FIX_THUMBNAIL": "Compress", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", - "INSTALL": "Install", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", - "NOT_FOUND": "404 - not found", - "LINK_EXPIRED": "Link expired", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", - "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", - "UPLOAD_DIRS": "Folder", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", - "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", - "ALL_ALBUMS": "All Albums", - "ALBUMS": "Albums", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", - "CREATE_ACCOUNT": "Create account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", - "YOU": "You", - "FAMILY": "Family", - "FREE": "free", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", - "AUTHENTICATE": "Authenticate", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", - "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", - "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", - "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", - "PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong", - "PREFERENCES": "Preferences", - "LANGUAGE": "Language", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" }, "AFTER_TIME": { - "HOUR": "after an hour", - "DAY": "after a day", - "WEEK": "after a week", - "MONTH": "after a month", - "YEAR": "after a year" + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" }, - "COPY_LINK": "Copy link", - "DONE": "Done", - "LINK_SHARE_TITLE": "Or share a link", - "REMOVE_LINK": "Remove link", - "CREATE_PUBLIC_SHARING": "Create public link", - "PUBLIC_LINK_CREATED": "Public link created", - "PUBLIC_LINK_ENABLED": "Public link enabled", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/ru-RU/translation.json b/web/apps/auth/public/locales/ru-RU/translation.json index 6870df319..c85db2236 100644 --- a/web/apps/auth/public/locales/ru-RU/translation.json +++ b/web/apps/auth/public/locales/ru-RU/translation.json @@ -1,644 +1,654 @@ { - "HERO_SLIDE_1_TITLE": "
Private backups
for your memories
", - "HERO_SLIDE_1": "End-to-end encrypted by default", - "HERO_SLIDE_2_TITLE": "
Safely stored
at a fallout shelter
", - "HERO_SLIDE_2": "Designed to outlive", - "HERO_SLIDE_3_TITLE": "
Available
everywhere
", - "HERO_SLIDE_3": "Android, iOS, Web, Desktop", - "LOGIN": "Login", - "SIGN_UP": "Signup", - "NEW_USER": "New to ente", - "EXISTING_USER": "Existing user", - "ENTER_NAME": "Enter name", - "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", - "ENTER_EMAIL": "Enter email address", - "EMAIL_ERROR": "Enter a valid email", - "REQUIRED": "Required", - "EMAIL_SENT": "Verification code sent to {{email}}", - "CHECK_INBOX": "Please check your inbox (and spam) to complete verification", - "ENTER_OTT": "Verification code", - "RESEND_MAIL": "Resend code", - "VERIFY": "Verify", - "UNKNOWN_ERROR": "Something went wrong, please try again", - "INVALID_CODE": "Invalid verification code", - "EXPIRED_CODE": "Your verification code has expired", - "SENDING": "Sending...", - "SENT": "Sent!", - "PASSWORD": "Password", - "LINK_PASSWORD": "Enter password to unlock the album", - "RETURN_PASSPHRASE_HINT": "Password", - "SET_PASSPHRASE": "Set password", - "VERIFY_PASSPHRASE": "Sign in", - "INCORRECT_PASSPHRASE": "Incorrect password", - "ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data", - "PASSPHRASE_DISCLAIMER": "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.", - "WELCOME_TO_ENTE_HEADING": "Welcome to ", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...", - "PASSPHRASE_HINT": "Password", - "CONFIRM_PASSPHRASE": "Confirm password", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", - "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", - "CREATE_COLLECTION": "New album", - "ENTER_ALBUM_NAME": "Album name", - "CLOSE_OPTION": "Close (Esc)", - "ENTER_FILE_NAME": "File name", - "CLOSE": "Close", - "NO": "No", - "NOTHING_HERE": "Nothing to see here yet 👀", - "UPLOAD": "Upload", - "IMPORT": "Import", - "ADD_PHOTOS": "Add photos", - "ADD_MORE_PHOTOS": "Add more photos", - "add_photos_one": "Add 1 item", - "add_photos_other": "Add {{count, number}} items", - "SELECT_PHOTOS": "Select photos", - "FILE_UPLOAD": "File Upload", + "HERO_SLIDE_1_TITLE": "
Личные резервные копии
для твоих воспоминаний
", + "HERO_SLIDE_1": "Сквозное шифрование по умолчанию", + "HERO_SLIDE_2_TITLE": "
Надежно хранится
в убежище от радиоактивных осадков
", + "HERO_SLIDE_2": "Созданный для того, чтобы пережить", + "HERO_SLIDE_3_TITLE": "
Доступно
везде
", + "HERO_SLIDE_3": "Android, iOS, Веб, ПК", + "LOGIN": "Авторизоваться", + "SIGN_UP": "Регистрация", + "NEW_USER": "Новенький в ente", + "EXISTING_USER": "Существующий пользователь", + "ENTER_NAME": "Введите имя", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Добавьте имя, чтобы ваши друзья знали, кого благодарить за эти замечательные фотографии!", + "ENTER_EMAIL": "Введите адрес электронной почты", + "EMAIL_ERROR": "Введите действительный адрес электронной почты", + "REQUIRED": "Требуется", + "EMAIL_SENT": "Проверочный код отправлен на
{{email}}", + "CHECK_INBOX": "Пожалуйста, проверьте свой почтовый ящик (и спам) для завершения проверки", + "ENTER_OTT": "Проверочный код", + "RESEND_MAIL": "Отправить код еще раз", + "VERIFY": "Подтвердить", + "UNKNOWN_ERROR": "Что-то пошло не так, Попробуйте еще раз", + "INVALID_CODE": "Неверный код подтверждения", + "EXPIRED_CODE": "Срок действия вашего проверочного кода истек", + "SENDING": "Отправка...", + "SENT": "Отправлено!", + "PASSWORD": "Пароль", + "LINK_PASSWORD": "Введите пароль, чтобы разблокировать альбом", + "RETURN_PASSPHRASE_HINT": "Пароль", + "SET_PASSPHRASE": "Установить пароль", + "VERIFY_PASSPHRASE": "Войти", + "INCORRECT_PASSPHRASE": "Неверный пароль", + "ENTER_ENC_PASSPHRASE": "Пожалуйста, введите пароль, который мы можем использовать для шифрования ваших данных", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "Добро пожаловать в ", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Где живут ваши лучшие фотографии", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Генерируем ключи шифрования...", + "PASSPHRASE_HINT": "Пароль", + "CONFIRM_PASSPHRASE": "Подтвердите пароль", + "REFERRAL_CODE_HINT": "Как вы узнали о Ente? (необязательно)", + "REFERRAL_INFO": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения!", + "PASSPHRASE_MATCH_ERROR": "Пароли не совпадают", + "CREATE_COLLECTION": "Новый альбом", + "ENTER_ALBUM_NAME": "Название альбома", + "CLOSE_OPTION": "Закрыть (Esc)", + "ENTER_FILE_NAME": "Имя файла", + "CLOSE": "Закрыть", + "NO": "Нет", + "NOTHING_HERE": "Здесь нечего смотреть! 👀", + "UPLOAD": "Загрузить", + "IMPORT": "Импорт", + "ADD_PHOTOS": "Добавить фотографии", + "ADD_MORE_PHOTOS": "Добавить больше фото", + "add_photos_one": "Добавить 1 элемент", + "add_photos_other": "Добавить {{count, number}} элементов", + "SELECT_PHOTOS": "Выбрать фотографии", + "FILE_UPLOAD": "Загрузка файла", "UPLOAD_STAGE_MESSAGE": { - "0": "Preparing to upload", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", - "5": "Backup complete" + "0": "Подготовка к загрузке", + "1": "Чтение файлов метаданных Google", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} файлов извлечены", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} файлов обработано", + "4": "Отмена оставшихся загрузок", + "5": "Резервное копирование завершено" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", - "SUBSCRIPTION_EXPIRED": "Subscription expired", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", - "STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", - "NO_ACCOUNT": "Don't have an account", - "ACCOUNT_EXISTS": "Already have an account", - "CREATE": "Create", - "DOWNLOAD": "Download", - "DOWNLOAD_OPTION": "Download (D)", - "DOWNLOAD_FAVORITES": "Download favorites", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", - "COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", - "ZOOM_IN_OUT": "Zoom in/out", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", - "UPLOAD_FIRST_PHOTO": "Upload your first photo", - "IMPORT_YOUR_FOLDERS": "Import your folders", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", - "TRASH_FILES_TITLE": "Delete files?", - "TRASH_FILE_TITLE": "Delete file?", - "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", - "DELETE": "Delete", - "DELETE_OPTION": "Delete (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album", - "OR": "or", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", - "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", - "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", - "CHANGE_PASSWORD": "Change password", - "GO_BACK": "Go back", - "RECOVERY_KEY": "Recovery key", - "SAVE_LATER": "Do this later", - "SAVE": "Save Key", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", - "FORGOT_PASSWORD": "Forgot password", - "RECOVER_ACCOUNT": "Recover account", - "RECOVERY_KEY_HINT": "Recovery key", - "RECOVER": "Recover", - "NO_RECOVERY_KEY": "No recovery key?", - "INCORRECT_RECOVERY_KEY": "Incorrect recovery key", - "SORRY": "Sorry", - "NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to {{emailID}} from your registered email address", - "CONTACT_SUPPORT": "Contact support", - "REQUEST_FEATURE": "Request Feature", - "SUPPORT": "Support", - "CONFIRM": "Confirm", - "CANCEL": "Cancel", - "LOGOUT": "Logout", - "DELETE_ACCOUNT": "Delete account", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", - "LOGOUT_MESSAGE": "Are you sure you want to logout?", - "CHANGE_EMAIL": "Change email", - "OK": "OK", - "SUCCESS": "Success", - "ERROR": "Error", - "MESSAGE": "Message", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", - "DOWNLOAD_APP": "Download desktop app", - "EXPORT": "Export Data", - "SUBSCRIPTION": "Subscription", - "SUBSCRIBE": "Subscribe", - "MANAGEMENT_PORTAL": "Manage payment method", - "MANAGE_FAMILY_PORTAL": "Manage family", - "LEAVE_FAMILY_PLAN": "Leave family plan", - "LEAVE": "Leave", - "LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?", - "CHOOSE_PLAN": "Choose your plan", - "MANAGE_PLAN": "Manage your subscription", - "ACTIVE": "Active", - "OFFLINE_MSG": "You are offline, cached memories are being shown", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", - "FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by", - "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please upgrade", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", - "SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again", - "SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", - "UPDATE_PAYMENT_METHOD": "Update payment method", - "MONTHLY": "Monthly", - "YEARLY": "Yearly", - "UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?", - "UPDATE_SUBSCRIPTION": "Change plan", - "CANCEL_SUBSCRIPTION": "Cancel subscription", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", - "REACTIVATE_SUBSCRIPTION": "Reactivate subscription", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", - "RENAME": "Rename", - "RENAME_FILE": "Rename file", - "RENAME_COLLECTION": "Rename album", - "DELETE_COLLECTION_TITLE": "Delete album?", - "DELETE_COLLECTION": "Delete album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", - "DELETE_PHOTOS": "Delete photos", - "KEEP_PHOTOS": "Keep photos", - "SHARE": "Share", - "SHARE_COLLECTION": "Share album", - "SHAREES": "Shared with", - "SHARE_WITH_SELF": "Oops, you cannot share with yourself", - "ALREADY_SHARED": "Oops, you're already sharing this with {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", - "DOWNLOAD_COLLECTION": "Download album", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", - "SEARCH": "Search", - "SEARCH_RESULTS": "Search results", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "FILE_NOT_UPLOADED_LIST": "Следующие файлы не были загружены", + "SUBSCRIPTION_EXPIRED": "Подписка закончилась", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Срок действия вашей подписки истек, пожалуйста, продлите", + "STORAGE_QUOTA_EXCEEDED": "Превышен лимит хранения", + "INITIAL_LOAD_DELAY_WARNING": "Первая загрузка может занять некоторое время", + "USER_DOES_NOT_EXIST": "Пользователь с таким email не найден", + "NO_ACCOUNT": "У вас нет учетной записи", + "ACCOUNT_EXISTS": "Уже есть аккаунт", + "CREATE": "Создать", + "DOWNLOAD": "Скачать", + "DOWNLOAD_OPTION": "Скачать (D)", + "DOWNLOAD_FAVORITES": "Скачать избранные", + "DOWNLOAD_UNCATEGORIZED": "Скачать без категорий", + "DOWNLOAD_HIDDEN_ITEMS": "Скачать скрытые элементы", + "COPY_OPTION": "Скопировать как PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Полноэкранный режим (F)", + "ZOOM_IN_OUT": "Увеличить/уменьшить", + "PREVIOUS": "Предыдущий (←)", + "NEXT": "Следующий (→)", + "TITLE_PHOTOS": "Ente Фото", + "TITLE_ALBUMS": "Ente Фото", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "Загрузите своё первое фото", + "IMPORT_YOUR_FOLDERS": "Импортируйте папки", + "UPLOAD_DROPZONE_MESSAGE": "Перетащите для резервного копирования файлов", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Перетащите, чтобы добавить просматриваемую папку", + "TRASH_FILES_TITLE": "Удалить файлы?", + "TRASH_FILE_TITLE": "Удалить файл?", + "DELETE_FILES_TITLE": "Удалить немедленно?", + "DELETE_FILES_MESSAGE": "Выбранные файлы будут безвозвратно удалены из вашей учетной записи ente.", + "DELETE": "Удалить", + "DELETE_OPTION": "Удалить (DEL)", + "FAVORITE_OPTION": "Избранное (L)", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "Обнаружено несколько папок", + "UPLOAD_STRATEGY_CHOICE": "Вы хотите загрузить их в", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Один альбом", + "OR": "или", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Отдельные альбомы", + "SESSION_EXPIRED_MESSAGE": "Истёк срок действия вашей сессии. Для продолжения, пожалуйста, войдите снова", + "SESSION_EXPIRED": "Время сессии истекло", + "PASSWORD_GENERATION_FAILED": "Вашему браузеру не удалось сгенерировать надежный ключ, соответствующий стандартам шифрования ente, пожалуйста, попробуйте использовать мобильное приложение или другой браузер", + "CHANGE_PASSWORD": "Изменить пароль", + "GO_BACK": "Вернуться назад", + "RECOVERY_KEY": "Ключ восстановления", + "SAVE_LATER": "Сделать позже", + "SAVE": "Сохранить ключ", + "RECOVERY_KEY_DESCRIPTION": "Если вы забыли свой пароль, то восстановить данные можно только с помощью этого ключа.", + "RECOVER_KEY_GENERATION_FAILED": "Не удалось сгенерировать код восстановления, пожалуйста, повторите попытку", + "KEY_NOT_STORED_DISCLAIMER": "Мы не храним этот ключ, поэтому, пожалуйста, сохраните его в надежном месте", + "FORGOT_PASSWORD": "Забыл пароль", + "RECOVER_ACCOUNT": "Восстановить аккаунт", + "RECOVERY_KEY_HINT": "Ключ восстановления", + "RECOVER": "Восстановить", + "NO_RECOVERY_KEY": "Нет ключа восстановления?", + "INCORRECT_RECOVERY_KEY": "Неправильный ключ восстановления", + "SORRY": "Извините", + "NO_RECOVERY_KEY_MESSAGE": "Из-за природы нашего сквозного протокола шифрования ваши данные не могут быть расшифрованы без вашего пароля или ключа восстановления", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Пожалуйста, отправьте электронное письмо на адрес {{emailID}} с вашего зарегистрированного адреса электронной почты", + "CONTACT_SUPPORT": "Связаться с поддержкой", + "REQUEST_FEATURE": "Запросить функцию", + "SUPPORT": "Поддержка", + "CONFIRM": "Подтвердить", + "CANCEL": "Отменить", + "LOGOUT": "Выйти", + "DELETE_ACCOUNT": "Удалить аккаунт", + "DELETE_ACCOUNT_MESSAGE": "

Пожалуйста, отправьте письмо по адресу {{emailID}} с вашего зарегистрированного адреса электронной почты.

Ваш запрос будет обработан в течение 72 часов

", + "LOGOUT_MESSAGE": "Вы уверены, что хотите выйти?", + "CHANGE_EMAIL": "Изменить адрес электронной почты", + "OK": "ОК", + "SUCCESS": "Успешно", + "ERROR": "Ошибка", + "MESSAGE": "Сообщение", + "INSTALL_MOBILE_APP": "Установите наше приложение Android или iOS для автоматического резервного копирования всех ваших фотографий", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "Загрузить приложение для компьютера", + "EXPORT": "Экспортировать данные", + "SUBSCRIPTION": "Подписка", + "SUBSCRIBE": "Подписаться", + "MANAGEMENT_PORTAL": "Управлять платёжной информацией", + "MANAGE_FAMILY_PORTAL": "Управление семьёй", + "LEAVE_FAMILY_PLAN": "Покинуть семейный план", + "LEAVE": "Выйти", + "LEAVE_FAMILY_CONFIRM": "Вы уверены, что хотите покинуть семейный план?", + "CHOOSE_PLAN": "Выбери свой план", + "MANAGE_PLAN": "Управление подпиской", + "ACTIVE": "Активный", + "OFFLINE_MSG": "Вы не в сети, кэшированные воспоминания отображаются", + "FREE_SUBSCRIPTION_INFO": "Вы используете бесплатный тарифный план, истекающий {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Вы используете семейный план, управляемый", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Продление {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "Обновить платёжную информацию", + "MONTHLY": "Ежемесячно", + "YEARLY": "Ежегодно", + "UPDATE_SUBSCRIPTION_MESSAGE": "Хотите сменить текущий план?", + "UPDATE_SUBSCRIPTION": "Изменить план", + "CANCEL_SUBSCRIPTION": "Отменить подписку", + "CANCEL_SUBSCRIPTION_MESSAGE": "

Все ваши данные будут удалены с наших серверов в конце этого расчетного периода.

Вы уверены, что хотите отменить свою подписку?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Вы уверены, что хотите отменить свою подписку?

", + "SUBSCRIPTION_CANCEL_FAILED": "Не удалось отменить подписку", + "SUBSCRIPTION_CANCEL_SUCCESS": "Подписка успешно отменена", + "REACTIVATE_SUBSCRIPTION": "Возобновить подписку", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "После повторной активации вам будет выставлен счет в {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Подписка успешно активирована ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Не удалось повторно активировать продление подписки", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Спасибо", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Отменить мобильную подписку", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Пожалуйста, отмените свою подписку в мобильном приложении, чтобы активировать подписку здесь", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Пожалуйста, свяжитесь с {{emailID}} для управления подпиской", + "RENAME": "Переименовать", + "RENAME_FILE": "Переименовать файл", + "RENAME_COLLECTION": "Переименовать альбом", + "DELETE_COLLECTION_TITLE": "Удалить альбом?", + "DELETE_COLLECTION": "Удалить альбом", + "DELETE_COLLECTION_MESSAGE": "Также удалить фотографии (и видео), которые есть в этом альбоме из всех других альбомов, где они есть?", + "DELETE_PHOTOS": "Удалить фото", + "KEEP_PHOTOS": "Оставить фото", + "SHARE": "Поделиться", + "SHARE_COLLECTION": "Поделиться альбомом", + "SHAREES": "Поделиться с", + "SHARE_WITH_SELF": "Ой, Вы не можете поделиться с самим собой", + "ALREADY_SHARED": "Упс, Вы уже делились этим с {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Делиться альбомом запрещено", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Совместное использование отключено для бесплатных аккаунтов", + "DOWNLOAD_COLLECTION": "Загрузить альбом", + "DOWNLOAD_COLLECTION_MESSAGE": "

Вы уверены, что хотите загрузить альбом полностью?

Все файлы будут последовательно помещены в очередь на загрузку

", + "CREATE_ALBUM_FAILED": "Не удалось создать альбом, пожалуйста, попробуйте еще раз", + "SEARCH": "Поиск", + "SEARCH_RESULTS": "Результаты поиска", + "NO_RESULTS": "Ничего не найдено", + "SEARCH_HINT": "Поиск альбомов, дат, описаний, ...", "SEARCH_TYPE": { - "COLLECTION": "Album", - "LOCATION": "Location", - "CITY": "Location", - "DATE": "Date", - "FILE_NAME": "File name", - "THING": "Content", - "FILE_CAPTION": "Description", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "COLLECTION": "Альбом", + "LOCATION": "Местоположение", + "CITY": "Местоположение", + "DATE": "Дата", + "FILE_NAME": "Имя файла", + "THING": "Содержимое", + "FILE_CAPTION": "Описание", + "FILE_TYPE": "Тип файла", + "CLIP": "" }, - "photos_count_zero": "No memories", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", - "ADD_TO_COLLECTION": "Add to album", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", - "PEOPLE": "People", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", - "INFO": "Info ", - "INFO_OPTION": "Info (I)", - "FILE_NAME": "File name", - "CAPTION_PLACEHOLDER": "Add a description", - "LOCATION": "Location", - "SHOW_ON_MAP": "View on OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", - "EXIF": "EXIF", - "ISO": "ISO", - "TWO_FACTOR": "Two-factor", - "TWO_FACTOR_AUTHENTICATION": "Two-factor authentication", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", - "ENTER_CODE_MANUALLY": "Enter the code manually", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", - "SCAN_QR_CODE": "Scan QR code instead", - "ENABLE_TWO_FACTOR": "Enable two-factor", - "ENABLE": "Enable", - "LOST_DEVICE": "Lost two-factor device", - "INCORRECT_CODE": "Incorrect code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", - "EXPORT_DATA": "Export data", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", - "SEND_OTT": "Send OTP", - "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", - "FAILED_UPLOADS": "Failed uploads ", - "SKIPPED_FILES": "Ignored uploads", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", - "SKIPPED_VIDEOS": "Skipped videos", - "INPROGRESS_METADATA_EXTRACTION": "In progress", - "INPROGRESS_UPLOADS": "Uploads in progress", - "TOO_LARGE_UPLOADS": "Large files", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan", - "TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", - "ARCHIVE": "Archive", - "FAVORITES": "Favorites", - "ARCHIVE_COLLECTION": "Archive album", - "ARCHIVE_SECTION_NAME": "Archive", - "ALL_SECTION_NAME": "All", - "MOVE_TO_COLLECTION": "Move to album", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", - "MOVE": "Move", - "ADD": "Add", - "REMOVE": "Remove", - "YES_REMOVE": "Yes, remove", - "REMOVE_FROM_COLLECTION": "Remove from album", - "TRASH": "Trash", - "MOVE_TO_TRASH": "Move to trash", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", - "DELETE_PERMANENTLY": "Delete permanently", - "RESTORE": "Restore", - "RESTORE_TO_COLLECTION": "Restore to album", - "EMPTY_TRASH": "Empty trash", - "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", - "LEAVE_SHARED_ALBUM": "Yes, leave", - "LEAVE_ALBUM": "Leave album", - "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", - "SORT_BY_CREATION_TIME_ASCENDING": "Oldest", - "SORT_BY_UPDATION_TIME_DESCENDING": "Last updated", - "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", - "FIX_THUMBNAIL": "Compress", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", - "INSTALL": "Install", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", - "NOT_FOUND": "404 - not found", - "LINK_EXPIRED": "Link expired", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", - "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", - "UPLOAD_DIRS": "Folder", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", - "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", - "ALL_ALBUMS": "All Albums", - "ALBUMS": "Albums", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", - "CREATE_ACCOUNT": "Create account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", - "YOU": "You", - "FAMILY": "Family", - "FREE": "free", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", - "AUTHENTICATE": "Authenticate", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", - "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", - "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", - "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", - "PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong", - "PREFERENCES": "Preferences", - "LANGUAGE": "Language", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "photos_count_zero": "Воспоминания отсутствуют", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "Добавить в альбом", + "SELECTED": "выбрано", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Это видео нельзя воспроизвести в вашем браузере", + "PEOPLE": "Люди", + "INDEXING_SCHEDULED": "Индексация запланирована...", + "ANALYZING_PHOTOS": "Индексирование фотографий ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Индексирование людей на {{indexStatus.nSyncedFiles,number}} фотографиях...", + "INDEXING_DONE": "Проиндексировано {{indexStatus.nSyncedFiles,number}} фотографий", + "UNIDENTIFIED_FACES": "нераспознанные лица", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "Двухфакторная аутентификация", + "TWO_FACTOR_QR_INSTRUCTION": "Сканируйте QR-код ниже с вашим любимым приложением для проверки подлинности", + "ENTER_CODE_MANUALLY": "Введите код вручную", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Пожалуйста, введите этот код в вашем любимом приложении для аутентификации", + "SCAN_QR_CODE": "Сканировать QR-код вместо", + "ENABLE_TWO_FACTOR": "Включить двухфакторную аутентификацию", + "ENABLE": "Включить", + "LOST_DEVICE": "Потеряно двухфакторное устройство", + "INCORRECT_CODE": "Неверный код", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "Отключить двухфакторную аутентификацию", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "Отключить", + "RECONFIGURE": "Перенастроить", + "UPDATE_TWO_FACTOR": "Обновить двухфакторную аутентификацию", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "Обновить", + "DISABLE_TWO_FACTOR": "Отключить двухфакторную аутентификацию", + "DISABLE_TWO_FACTOR_MESSAGE": "Вы уверены, что хотите отключить двухфакторную аутентификацию", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "Экспортировать данные", + "SELECT_FOLDER": "Выбрать папку", + "DESTINATION": "Место назначения", + "START": "Начать", + "LAST_EXPORT_TIME": "Время последнего экспорта", + "EXPORT_AGAIN": "Синхронизировать заново", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Локальное хранилище недоступно", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "Отправить одноразовый код", + "EMAIl_ALREADY_OWNED": "Почта уже использована", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" }, "AFTER_TIME": { - "HOUR": "after an hour", - "DAY": "after a day", - "WEEK": "after a week", - "MONTH": "after a month", - "YEAR": "after a year" + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" }, - "COPY_LINK": "Copy link", - "DONE": "Done", - "LINK_SHARE_TITLE": "Or share a link", - "REMOVE_LINK": "Remove link", - "CREATE_PUBLIC_SHARING": "Create public link", - "PUBLIC_LINK_CREATED": "Public link created", - "PUBLIC_LINK_ENABLED": "Public link enabled", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "Скрыть", + "UNHIDE": "Показать", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "Сортировать по", + "NEWEST_FIRST": "Сначала новые", + "OLDEST_FIRST": "Сначала старые", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "Выбрать альбом", + "PIN_ALBUM": "Закрепить альбом", + "UNPIN_ALBUM": "Открепить альбом", + "DOWNLOAD_COMPLETE": "Загрузка завершена", + "DOWNLOADING_COLLECTION": "Загрузка {{name}}", + "DOWNLOAD_FAILED": "Загрузка не удалась", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} файлов", + "CHRISTMAS": "Рождество", + "CHRISTMAS_EVE": "Канун Рождества", + "NEW_YEAR": "Новый год", + "NEW_YEAR_EVE": "Канун Нового года", + "IMAGE": "Изображение", + "VIDEO": "Видео", + "LIVE_PHOTO": "Живое фото", + "CONVERT": "Преобразовать", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Вы уверены, что хотите закрыть редактор?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Загрузите отредактированное изображение или сохраните копию в ente, чтобы сохранить внесенные изменения.", + "BRIGHTNESS": "Яркость", + "CONTRAST": "Контраст", + "SATURATION": "Насыщенность", + "BLUR": "Размытие", + "INVERT_COLORS": "Инвертировать Цвета", + "ASPECT_RATIO": "Соотношение Сторон", + "SQUARE": "Квадрат", + "ROTATE_LEFT": "Повернуть влево", + "ROTATE_RIGHT": "Повернуть вправо", + "FLIP_VERTICALLY": "Отразить вертикально", + "FLIP_HORIZONTALLY": "Отразить горизонтально", + "DOWNLOAD_EDITED": "Скачать отредактированный", + "SAVE_A_COPY_TO_ENTE": "Сохранить копию в ente", + "RESTORE_ORIGINAL": "Восстановить оригинал", + "TRANSFORM": "Преобразовать", + "COLORS": "Цвета", + "FLIP": "Перевернуть", + "ROTATION": "", + "RESET": "Сбросить", + "PHOTO_EDITOR": "Редактор фото", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "Статус волшебного поиска", + "INDEXED_ITEMS": "Индексированные элементы", + "CAST_ALBUM_TO_TV": "Воспроизвести альбом на ТВ", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/sv-SE/translation.json b/web/apps/auth/public/locales/sv-SE/translation.json new file mode 100644 index 000000000..f88535795 --- /dev/null +++ b/web/apps/auth/public/locales/sv-SE/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "Ange namn", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "Ange e-postadress", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "Lösenord", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "Lösenord", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "Logga in", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "Välkommen till ", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "Lösenord", + "CONFIRM_PASSPHRASE": "Bekräfta lösenord", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "Lösenorden matchar inte", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "Filnamn", + "CLOSE": "Stäng", + "NO": "Nej", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "Radera", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "eller", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "Ändra lösenord", + "GO_BACK": "", + "RECOVERY_KEY": "Återställningsnyckel", + "SAVE_LATER": "", + "SAVE": "Spara nyckel", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "Glömt lösenord", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "Ingen återställningsnyckel?", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "Bekräfta", + "CANCEL": "Avbryt", + "LOGOUT": "Logga ut", + "DELETE_ACCOUNT": "Radera konto", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "Meddelande", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "Prenumerera", + "MANAGEMENT_PORTAL": "Hantera betalningsmetod", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "Hantera din prenumeration", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "Sök", + "SEARCH_RESULTS": "Sökresultat", + "NO_RESULTS": "Inga resultat hittades", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "Datum", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "Beskrivning", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "Inga deltagare", + "participants_one": "1 deltagare", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "Deltagare", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "Fil", + "UPLOAD_DIRS": "Mapp", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "Filer", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "1 album", + "albums_other": "{{count, number}} album", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "Lägg till mapp", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "mån", + "YEAR": "år", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "GB", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "Namn...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "Språk", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "Kopiera länk", + "DONE": "Klar", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "Sortera efter", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "Bild", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "Färger", + "FLIP": "", + "ROTATION": "", + "RESET": "Återställ", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/auth/public/locales/th-TH/translation.json b/web/apps/auth/public/locales/th-TH/translation.json new file mode 100644 index 000000000..888ed7093 --- /dev/null +++ b/web/apps/auth/public/locales/th-TH/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/auth/public/locales/tr-TR/translation.json b/web/apps/auth/public/locales/tr-TR/translation.json index 6870df319..888ed7093 100644 --- a/web/apps/auth/public/locales/tr-TR/translation.json +++ b/web/apps/auth/public/locales/tr-TR/translation.json @@ -1,644 +1,654 @@ { - "HERO_SLIDE_1_TITLE": "
Private backups
for your memories
", - "HERO_SLIDE_1": "End-to-end encrypted by default", - "HERO_SLIDE_2_TITLE": "
Safely stored
at a fallout shelter
", - "HERO_SLIDE_2": "Designed to outlive", - "HERO_SLIDE_3_TITLE": "
Available
everywhere
", - "HERO_SLIDE_3": "Android, iOS, Web, Desktop", - "LOGIN": "Login", - "SIGN_UP": "Signup", - "NEW_USER": "New to ente", - "EXISTING_USER": "Existing user", - "ENTER_NAME": "Enter name", - "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", - "ENTER_EMAIL": "Enter email address", - "EMAIL_ERROR": "Enter a valid email", - "REQUIRED": "Required", - "EMAIL_SENT": "Verification code sent to
{{email}}", - "CHECK_INBOX": "Please check your inbox (and spam) to complete verification", - "ENTER_OTT": "Verification code", - "RESEND_MAIL": "Resend code", - "VERIFY": "Verify", - "UNKNOWN_ERROR": "Something went wrong, please try again", - "INVALID_CODE": "Invalid verification code", - "EXPIRED_CODE": "Your verification code has expired", - "SENDING": "Sending...", - "SENT": "Sent!", - "PASSWORD": "Password", - "LINK_PASSWORD": "Enter password to unlock the album", - "RETURN_PASSPHRASE_HINT": "Password", - "SET_PASSPHRASE": "Set password", - "VERIFY_PASSPHRASE": "Sign in", - "INCORRECT_PASSPHRASE": "Incorrect password", - "ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data", - "PASSPHRASE_DISCLAIMER": "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.", - "WELCOME_TO_ENTE_HEADING": "Welcome to ", - "WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...", - "PASSPHRASE_HINT": "Password", - "CONFIRM_PASSPHRASE": "Confirm password", - "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", - "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", - "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", - "CREATE_COLLECTION": "New album", - "ENTER_ALBUM_NAME": "Album name", - "CLOSE_OPTION": "Close (Esc)", - "ENTER_FILE_NAME": "File name", - "CLOSE": "Close", - "NO": "No", - "NOTHING_HERE": "Nothing to see here yet 👀", - "UPLOAD": "Upload", - "IMPORT": "Import", - "ADD_PHOTOS": "Add photos", - "ADD_MORE_PHOTOS": "Add more photos", - "add_photos_one": "Add 1 item", - "add_photos_other": "Add {{count, number}} items", - "SELECT_PHOTOS": "Select photos", - "FILE_UPLOAD": "File Upload", + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", "UPLOAD_STAGE_MESSAGE": { - "0": "Preparing to upload", - "1": "Reading google metadata files", - "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted", - "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed", - "4": "Cancelling remaining uploads", - "5": "Backup complete" + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" }, - "FILE_NOT_UPLOADED_LIST": "The following files were not uploaded", - "SUBSCRIPTION_EXPIRED": "Subscription expired", - "SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please renew", - "STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded", - "INITIAL_LOAD_DELAY_WARNING": "First load may take some time", - "USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email", - "NO_ACCOUNT": "Don't have an account", - "ACCOUNT_EXISTS": "Already have an account", - "CREATE": "Create", - "DOWNLOAD": "Download", - "DOWNLOAD_OPTION": "Download (D)", - "DOWNLOAD_FAVORITES": "Download favorites", - "DOWNLOAD_UNCATEGORIZED": "Download uncategorized", - "DOWNLOAD_HIDDEN_ITEMS": "Download hidden items", - "COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)", - "TOGGLE_FULLSCREEN": "Toggle fullscreen (F)", - "ZOOM_IN_OUT": "Zoom in/out", - "PREVIOUS": "Previous (←)", - "NEXT": "Next (→)", - "TITLE_PHOTOS": "Ente Photos", - "TITLE_ALBUMS": "Ente Photos", - "TITLE_AUTH": "Ente Auth", - "UPLOAD_FIRST_PHOTO": "Upload your first photo", - "IMPORT_YOUR_FOLDERS": "Import your folders", - "UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files", - "WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder", - "TRASH_FILES_TITLE": "Delete files?", - "TRASH_FILE_TITLE": "Delete file?", - "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", - "DELETE": "Delete", - "DELETE_OPTION": "Delete (DEL)", - "FAVORITE_OPTION": "Favorite (L)", - "UNFAVORITE_OPTION": "Unfavorite (L)", - "MULTI_FOLDER_UPLOAD": "Multiple folders detected", - "UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album", - "OR": "or", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", - "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", - "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", - "CHANGE_PASSWORD": "Change password", - "GO_BACK": "Go back", - "RECOVERY_KEY": "Recovery key", - "SAVE_LATER": "Do this later", - "SAVE": "Save Key", - "RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.", - "RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again", - "KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place", - "FORGOT_PASSWORD": "Forgot password", - "RECOVER_ACCOUNT": "Recover account", - "RECOVERY_KEY_HINT": "Recovery key", - "RECOVER": "Recover", - "NO_RECOVERY_KEY": "No recovery key?", - "INCORRECT_RECOVERY_KEY": "Incorrect recovery key", - "SORRY": "Sorry", - "NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to {{emailID}} from your registered email address", - "CONTACT_SUPPORT": "Contact support", - "REQUEST_FEATURE": "Request Feature", - "SUPPORT": "Support", - "CONFIRM": "Confirm", - "CANCEL": "Cancel", - "LOGOUT": "Logout", - "DELETE_ACCOUNT": "Delete account", - "DELETE_ACCOUNT_MESSAGE": "

Please send an email to {{emailID}} from your registered email address.

Your request will be processed within 72 hours.

", - "LOGOUT_MESSAGE": "Are you sure you want to logout?", - "CHANGE_EMAIL": "Change email", - "OK": "OK", - "SUCCESS": "Success", - "ERROR": "Error", - "MESSAGE": "Message", - "INSTALL_MOBILE_APP": "Install our Android or iOS app to automatically backup all your photos", - "DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app", - "DOWNLOAD_APP": "Download desktop app", - "EXPORT": "Export Data", - "SUBSCRIPTION": "Subscription", - "SUBSCRIBE": "Subscribe", - "MANAGEMENT_PORTAL": "Manage payment method", - "MANAGE_FAMILY_PORTAL": "Manage family", - "LEAVE_FAMILY_PLAN": "Leave family plan", - "LEAVE": "Leave", - "LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?", - "CHOOSE_PLAN": "Choose your plan", - "MANAGE_PLAN": "Manage your subscription", - "ACTIVE": "Active", - "OFFLINE_MSG": "You are offline, cached memories are being shown", - "FREE_SUBSCRIPTION_INFO": "You are on the free plan that expires on {{date, dateTime}}", - "FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by", - "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please upgrade", - "SUBSCRIPTION_PURCHASE_SUCCESS": "

We've received your payment

Your subscription is valid till {{date, dateTime}}

", - "SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe", - "SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again", - "SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again", - "UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again", - "STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again", - "UPDATE_PAYMENT_METHOD": "Update payment method", - "MONTHLY": "Monthly", - "YEARLY": "Yearly", - "UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?", - "UPDATE_SUBSCRIPTION": "Change plan", - "CANCEL_SUBSCRIPTION": "Cancel subscription", - "CANCEL_SUBSCRIPTION_MESSAGE": "

All of your data will be deleted from our servers at the end of this billing period.

Are you sure that you want to cancel your subscription?

", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Are you sure you want to cancel your subscription?

", - "SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription", - "SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully", - "REACTIVATE_SUBSCRIPTION": "Reactivate subscription", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ", - "SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here", - "MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at {{emailID}} to manage your subscription", - "RENAME": "Rename", - "RENAME_FILE": "Rename file", - "RENAME_COLLECTION": "Rename album", - "DELETE_COLLECTION_TITLE": "Delete album?", - "DELETE_COLLECTION": "Delete album", - "DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from all other albums they are part of?", - "DELETE_PHOTOS": "Delete photos", - "KEEP_PHOTOS": "Keep photos", - "SHARE": "Share", - "SHARE_COLLECTION": "Share album", - "SHAREES": "Shared with", - "SHARE_WITH_SELF": "Oops, you cannot share with yourself", - "ALREADY_SHARED": "Oops, you're already sharing this with {{email}}", - "SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts", - "DOWNLOAD_COLLECTION": "Download album", - "DOWNLOAD_COLLECTION_MESSAGE": "

Are you sure you want to download the complete album?

All files will be queued for download sequentially

", - "CREATE_ALBUM_FAILED": "Failed to create album , please try again", - "SEARCH": "Search", - "SEARCH_RESULTS": "Search results", - "NO_RESULTS": "No results found", - "SEARCH_HINT": "Search for albums, dates, descriptions, ...", + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", "SEARCH_TYPE": { - "COLLECTION": "Album", - "LOCATION": "Location", - "CITY": "Location", - "DATE": "Date", - "FILE_NAME": "File name", - "THING": "Content", - "FILE_CAPTION": "Description", - "FILE_TYPE": "File type", - "CLIP": "Magic" + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" }, - "photos_count_zero": "No memories", - "photos_count_one": "1 memory", - "photos_count_other": "{{count, number}} memories", - "TERMS_AND_CONDITIONS": "I agree to the terms and privacy policy", - "ADD_TO_COLLECTION": "Add to album", - "SELECTED": "selected", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser", - "PEOPLE": "People", - "INDEXING_SCHEDULED": "Indexing is scheduled...", - "ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", - "INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...", - "INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos", - "UNIDENTIFIED_FACES": "unidentified faces", - "OBJECTS": "objects", - "TEXT": "text", - "INFO": "Info ", - "INFO_OPTION": "Info (I)", - "FILE_NAME": "File name", - "CAPTION_PLACEHOLDER": "Add a description", - "LOCATION": "Location", - "SHOW_ON_MAP": "View on OpenStreetMap", - "MAP": "Map", - "MAP_SETTINGS": "Map Settings", - "ENABLE_MAPS": "Enable Maps?", - "ENABLE_MAP": "Enable map", - "DISABLE_MAPS": "Disable Maps?", - "ENABLE_MAP_DESCRIPTION": "

This will show your photos on a world map.

The map is hosted by OpenStreetMap, and the exact locations of your photos are never shared.

You can disable this feature anytime from Settings.

", - "DISABLE_MAP_DESCRIPTION": "

This will disable the display of your photos on a world map.

You can enable this feature anytime from Settings.

", - "DISABLE_MAP": "Disable map", - "DETAILS": "Details", - "VIEW_EXIF": "View all EXIF data", - "NO_EXIF": "No EXIF data", - "EXIF": "EXIF", - "ISO": "ISO", - "TWO_FACTOR": "Two-factor", - "TWO_FACTOR_AUTHENTICATION": "Two-factor authentication", - "TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app", - "ENTER_CODE_MANUALLY": "Enter the code manually", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app", - "SCAN_QR_CODE": "Scan QR code instead", - "ENABLE_TWO_FACTOR": "Enable two-factor", - "ENABLE": "Enable", - "LOST_DEVICE": "Lost two-factor device", - "INCORRECT_CODE": "Incorrect code", - "TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account", - "DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication", - "UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device", - "DISABLE": "Disable", - "RECONFIGURE": "Reconfigure", - "UPDATE_TWO_FACTOR": "Update two-factor", - "UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators", - "UPDATE": "Update", - "DISABLE_TWO_FACTOR": "Disable two-factor", - "DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication", - "TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again", - "EXPORT_DATA": "Export data", - "SELECT_FOLDER": "Select folder", - "DESTINATION": "Destination", - "START": "Start", - "LAST_EXPORT_TIME": "Last export time", - "EXPORT_AGAIN": "Resync", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", - "SEND_OTT": "Send OTP", - "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", - "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", - "RETRY_FAILED": "Retry failed uploads", - "FAILED_UPLOADS": "Failed uploads ", - "SKIPPED_FILES": "Ignored uploads", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed", - "UNSUPPORTED_FILES": "Unsupported files", - "SUCCESSFUL_UPLOADS": "Successful uploads", - "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", - "BLOCKED_UPLOADS": "Blocked uploads", - "SKIPPED_VIDEOS": "Skipped videos", - "INPROGRESS_METADATA_EXTRACTION": "In progress", - "INPROGRESS_UPLOADS": "Uploads in progress", - "TOO_LARGE_UPLOADS": "Large files", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan", - "TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit", - "THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.", - "UPLOAD_TO_COLLECTION": "Upload to album", - "UNCATEGORIZED": "Uncategorized", - "ARCHIVE": "Archive", - "FAVORITES": "Favorites", - "ARCHIVE_COLLECTION": "Archive album", - "ARCHIVE_SECTION_NAME": "Archive", - "ALL_SECTION_NAME": "All", - "MOVE_TO_COLLECTION": "Move to album", - "UNARCHIVE": "Unarchive", - "UNARCHIVE_COLLECTION": "Unarchive album", - "HIDE_COLLECTION": "Hide album", - "UNHIDE_COLLECTION": "Unhide album", - "MOVE": "Move", - "ADD": "Add", - "REMOVE": "Remove", - "YES_REMOVE": "Yes, remove", - "REMOVE_FROM_COLLECTION": "Remove from album", - "TRASH": "Trash", - "MOVE_TO_TRASH": "Move to trash", - "TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.", - "TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.", - "DELETE_PERMANENTLY": "Delete permanently", - "RESTORE": "Restore", - "RESTORE_TO_COLLECTION": "Restore to album", - "EMPTY_TRASH": "Empty trash", - "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", - "LEAVE_SHARED_ALBUM": "Yes, leave", - "LEAVE_ALBUM": "Leave album", - "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", - "LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.", - "NOT_FILE_OWNER": "You cannot delete files in a shared album", - "CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.", - "SORT_BY_CREATION_TIME_ASCENDING": "Oldest", - "SORT_BY_UPDATION_TIME_DESCENDING": "Last updated", - "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "Compress thumbnails", - "THUMBNAIL_REPLACED": "Thumbnails compressed", - "FIX_THUMBNAIL": "Compress", - "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", - "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", - "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", - "FIX_CREATION_TIME": "Fix time", - "FIX_CREATION_TIME_IN_PROGRESS": "Fixing time", - "CREATION_TIME_UPDATED": "File time updated", - "UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use", - "UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry", - "CAPTION_CHARACTER_LIMIT": "5000 characters max", - "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", - "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", - "METADATA_DATE": "EXIF:MetadataDate", - "CUSTOM_TIME": "Custom time", - "REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans", - "INSTALL": "Install", - "SHARING_DETAILS": "Sharing details", - "MODIFY_SHARING": "Modify sharing", - "ADD_COLLABORATORS": "Add collaborators", - "ADD_NEW_EMAIL": "Add a new email", - "shared_with_people_zero": "Share with specific people", - "shared_with_people_one": "Shared with 1 person", - "shared_with_people_other": "Shared with {{count, number}} people", - "participants_zero": "No participants", - "participants_one": "1 participant", - "participants_other": "{{count, number}} participants", - "ADD_VIEWERS": "Add viewers", - "PARTICIPANTS": "Participants", - "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} will not be able to add more photos to the album

They will still be able to remove photos added by them

", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album", - "CONVERT_TO_VIEWER": "Yes, convert to viewer", - "CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator", - "CHANGE_PERMISSION": "Change permission?", - "REMOVE_PARTICIPANT": "Remove?", - "CONFIRM_REMOVE": "Yes, remove", - "MANAGE": "Manage", - "ADDED_AS": "Added as", - "COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album", - "REMOVE_PARTICIPANT_HEAD": "Remove participant", - "OWNER": "Owner", - "COLLABORATORS": "Collaborators", - "ADD_MORE": "Add more", - "VIEWERS": "Viewers", - "OR_ADD_EXISTING": "Or pick an existing one", - "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} will be removed from the album

Any photos added by them will also be removed from the album

", - "NOT_FOUND": "404 - not found", - "LINK_EXPIRED": "Link expired", - "LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!", - "MANAGE_LINK": "Manage link", - "LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!", - "FILE_DOWNLOAD": "Allow downloads", - "LINK_PASSWORD_LOCK": "Password lock", - "PUBLIC_COLLECT": "Allow adding photos", - "LINK_DEVICE_LIMIT": "Device limit", - "NO_DEVICE_LIMIT": "None", - "LINK_EXPIRY": "Link expiry", - "NEVER": "Never", - "DISABLE_FILE_DOWNLOAD": "Disable download", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Are you sure that you want to disable the download button for files?

Viewers can still take screenshots or save a copy of your photos using external tools.

", - "MALICIOUS_CONTENT": "Contains malicious content", - "COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent", - "SHARED_USING": "Shared using ", - "ENTE_IO": "ente.io", - "SHARING_REFERRAL_CODE": "Use code {{referralCode}} to get 10 GB free", - "LIVE": "LIVE", - "DISABLE_PASSWORD": "Disable password lock", - "DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?", - "PASSWORD_LOCK": "Password lock", - "LOCK": "Lock", - "DOWNLOAD_UPLOAD_LOGS": "Debug logs", - "UPLOAD_FILES": "File", - "UPLOAD_DIRS": "Folder", - "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", - "DEDUPLICATE_FILES": "Deduplicate files", - "AUTHENTICATOR_SECTION": "Authenticator", - "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", - "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", - "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", - "STOP_UPLOADS_HEADER": "Stop uploads?", - "YES_STOP_UPLOADS": "Yes, stop uploads", - "STOP_DOWNLOADS_HEADER": "Stop downloads?", - "YES_STOP_DOWNLOADS": "Yes, stop downloads", - "STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?", - "albums_one": "1 Album", - "albums_other": "{{count, number}} Albums", - "ALL_ALBUMS": "All Albums", - "ALBUMS": "Albums", - "ALL_HIDDEN_ALBUMS": "All hidden albums", - "HIDDEN_ALBUMS": "Hidden albums", - "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", - "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", - "CREATE_ACCOUNT": "Create account", - "COPIED": "Copied", - "CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail", - "CANVAS_BLOCKED_MESSAGE": "

It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos

Please enable access to your browser's canvas, or check out our desktop app

", - "WATCH_FOLDERS": "Watch folders", - "UPGRADE_NOW": "Upgrade now", - "RENEW_NOW": "Renew now", - "STORAGE": "Storage", - "USED": "used", - "YOU": "You", - "FAMILY": "Family", - "FREE": "free", - "OF": "of", - "WATCHED_FOLDERS": "Watched folders", - "NO_FOLDERS_ADDED": "No folders added yet!", - "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", - "ADD_FOLDER": "Add folder", - "STOP_WATCHING": "Stop watching", - "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", - "YES_STOP": "Yes, stop", - "MONTH_SHORT": "mo", - "YEAR": "year", - "FAMILY_PLAN": "Family plan", - "DOWNLOAD_LOGS": "Download logs", - "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", - "CHANGE_FOLDER": "Change Folder", - "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", - "POPULAR": "Popular", - "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", - "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", - "AUTHENTICATE": "Authenticate", - "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", - "NEVERMIND": "Nevermind", - "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", - "INSTALL_NOW": "Install now", - "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", - "DOWNLOAD_AND_INSTALL": "Download and install", - "IGNORE_THIS_VERSION": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", - "NAME_PLACEHOLDER": "Name...", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

You have dragged and dropped a mixture of files and folders.

Please provide either only files, or only folders when selecting option to create separate albums

", - "CHOSE_THEME": "Choose theme", - "ML_SEARCH": "Face recognition", - "ENABLE_ML_SEARCH_DESCRIPTION": "

This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.

For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.

If this is the first time you're enabling this, we'll also ask your permission to process face data.

", - "ML_MORE_DETAILS": "More details", - "ENABLE_FACE_SEARCH": "Enable face recognition", - "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", - "DISABLE_BETA": "Pause recognition", - "DISABLE_FACE_SEARCH": "Disable face recognition", - "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", - "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", - "LABS": "Labs", - "YOURS": "yours", - "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", - "PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate", - "PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong", - "PREFERENCES": "Preferences", - "LANGUAGE": "Language", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", - "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" }, "AFTER_TIME": { - "HOUR": "after an hour", - "DAY": "after a day", - "WEEK": "after a week", - "MONTH": "after a month", - "YEAR": "after a year" + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" }, - "COPY_LINK": "Copy link", - "DONE": "Done", - "LINK_SHARE_TITLE": "Or share a link", - "REMOVE_LINK": "Remove link", - "CREATE_PUBLIC_SHARING": "Create public link", - "PUBLIC_LINK_CREATED": "Public link created", - "PUBLIC_LINK_ENABLED": "Public link enabled", - "COLLECT_PHOTOS": "Collect photos", - "PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.", - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", "EXPORT_NOTIFICATION": { - "START": "Export started", - "IN_PROGRESS": "Export already in progress", - "FINISH": "Export finished", - "UP_TO_DATE": "No new files to export" + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" }, - "CONTINUOUS_EXPORT": "Sync continuously", - "TOTAL_ITEMS": "Total items", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", - "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", "DELETE_REASON": { - "MISSING_FEATURE": "It's missing a key feature that I need", - "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", - "FOUND_ANOTHER_SERVICE": "I found another service that I like better", - "NOT_LISTED": "My reason isn't listed" + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", - "FEEDBACK_REQUIRED": "Kindly help us with this information", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?", - "RECOVER_TWO_FACTOR": "Recover two-factor", - "at": "at", - "AUTH_NEXT": "next", - "AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets", - "HIDDEN": "Hidden", - "HIDE": "Hide", - "UNHIDE": "Unhide", - "UNHIDE_TO_COLLECTION": "Unhide to album", - "SORT_BY": "Sort by", - "NEWEST_FIRST": "Newest first", - "OLDEST_FIRST": "Oldest first", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.", - "SELECT_COLLECTION": "Select album", - "PIN_ALBUM": "Pin album", - "UNPIN_ALBUM": "Unpin album", - "DOWNLOAD_COMPLETE": "Download complete", - "DOWNLOADING_COLLECTION": "Downloading {{name}}", - "DOWNLOAD_FAILED": "Download failed", - "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", - "IMAGE": "Image", - "VIDEO": "Video", - "LIVE_PHOTO": "Live Photo", - "CONVERT": "Convert", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", - "BRIGHTNESS": "Brightness", - "CONTRAST": "Contrast", - "SATURATION": "Saturation", - "BLUR": "Blur", - "INVERT_COLORS": "Invert Colors", - "ASPECT_RATIO": "Aspect Ratio", - "SQUARE": "Square", - "ROTATE_LEFT": "Rotate Left", - "ROTATE_RIGHT": "Rotate Right", - "FLIP_VERTICALLY": "Flip Vertically", - "FLIP_HORIZONTALLY": "Flip Horizontally", - "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", - "RESTORE_ORIGINAL": "Restore Original", - "TRANSFORM": "Transform", - "COLORS": "Colors", - "FLIP": "Flip", - "ROTATION": "Rotation", - "RESET": "Reset", - "PHOTO_EDITOR": "Photo Editor", - "FASTER_UPLOAD": "Faster uploads", - "FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers", - "MAGIC_SEARCH_STATUS": "Magic Search Status", - "INDEXED_ITEMS": "Indexed items", - "CAST_ALBUM_TO_TV": "Play album on TV", - "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", - "PAIR_DEVICE_TO_TV": "Pair devices", - "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", - "PAIR_WITH_PIN": "Pair with PIN", - "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", - "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", - "FREEHAND": "Freehand", - "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/public/locales/zh-CN/translation.json b/web/apps/auth/public/locales/zh-CN/translation.json index 15ef565dd..0a72e1c70 100644 --- a/web/apps/auth/public/locales/zh-CN/translation.json +++ b/web/apps/auth/public/locales/zh-CN/translation.json @@ -9,7 +9,7 @@ "SIGN_UP": "注册", "NEW_USER": "刚来到 ente", "EXISTING_USER": "现有用户", - "ENTER_NAME": "现有用户", + "ENTER_NAME": "输入名字", "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", "ENTER_EMAIL": "请输入电子邮件地址", "EMAIL_ERROR": "请输入有效的电子邮件", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "您是如何知道Ente的? (可选的)", "REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致", - "CONSOLE_WARNING_STOP": "停止!", - "CONSOLE_WARNING_DESC": "这是专为开发人员设计的浏览器功能。 请不要在此处复制粘贴未经验证的代码。", "CREATE_COLLECTION": "新建相册", "ENTER_ALBUM_NAME": "相册名称", "CLOSE_OPTION": "关闭 (或按Esc键)", @@ -85,9 +83,9 @@ "ZOOM_IN_OUT": "放大/缩小", "PREVIOUS": "上一个 (←)", "NEXT": "下一个 (→)", - "TITLE_PHOTOS": "ente 照片", - "TITLE_ALBUMS": "ente 照片", - "TITLE_AUTH": "ente 验证器", + "TITLE_PHOTOS": "Ente 照片", + "TITLE_ALBUMS": "Ente 照片", + "TITLE_AUTH": "Ente 验证器", "UPLOAD_FIRST_PHOTO": "上传您的第一张照片", "IMPORT_YOUR_FOLDERS": "导入您的文件夹", "UPLOAD_DROPZONE_MESSAGE": "拖放以备份您的文件", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "正在下载 {{name}}", "DOWNLOAD_FAILED": "下载失败", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} 个文件", - "CRASH_REPORTING": "崩溃报告", "CHRISTMAS": "圣诞", "CHRISTMAS_EVE": "平安夜", "NEW_YEAR": "新年", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 cast.ente.io 。", "CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。", "CACHE_DIRECTORY": "缓存文件夹", - "PASSKEYS": "通行密钥", "FREEHAND": "手画", "APPLY_CROP": "应用裁剪", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。", + "PASSKEYS": "通行密钥", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index c06531ab4..891dde1dd 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -1,7 +1,9 @@ -import AppNavbar from "@ente/shared/components/Navbar/app"; -import { t } from "i18next"; -import { createContext, useEffect, useRef, useState } from "react"; - +import { setupI18n } from "@/next/i18n"; +import { + APPS, + APP_TITLES, + CLIENT_PACKAGE_NAMES, +} from "@ente/shared/apps/constants"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import { @@ -10,32 +12,26 @@ import { } from "@ente/shared/components/DialogBoxV2/types"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { MessageContainer } from "@ente/shared/components/MessageContainer"; +import AppNavbar from "@ente/shared/components/Navbar/app"; +import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; +import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { clearLogsIfLocalStorageLimitExceeded, logStartupMessage, } from "@ente/shared/logging/web"; import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS } from "@ente/shared/storage/localStorage"; -import { CssBaseline, useMediaQuery } from "@mui/material"; -import { ThemeProvider } from "@mui/material/styles"; -import Head from "next/head"; -import { useRouter } from "next/router"; -import LoadingBar from "react-top-loading-bar"; - -import { setupI18n } from "@/ui/i18n"; -import { CacheProvider } from "@emotion/react"; -import { - APP_TITLES, - APPS, - CLIENT_PACKAGE_NAMES, -} from "@ente/shared/apps/constants"; -import { EnteAppProps } from "@ente/shared/apps/types"; -import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; -import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; -import createEmotionCache from "@ente/shared/themes/createEmotionCache"; import { SetTheme } from "@ente/shared/themes/types"; +import { CssBaseline, useMediaQuery } from "@mui/material"; +import { ThemeProvider } from "@mui/material/styles"; +import { t } from "i18next"; +import { AppProps } from "next/app"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { createContext, useEffect, useRef, useState } from "react"; +import LoadingBar from "react-top-loading-bar"; import "../../public/css/global.css"; type AppContextType = { @@ -51,15 +47,8 @@ type AppContextType = { export const AppContext = createContext(null); -// Client-side cache, shared for the whole session of the user in the browser. -const clientSideEmotionCache = createEmotionCache(); - -export default function App(props: EnteAppProps) { - const { - Component, - emotionCache = clientSideEmotionCache, - pageProps, - } = props; +export default function App(props: AppProps) { + const { Component, pageProps } = props; const router = useRouter(); const [isI18nReady, setIsI18nReady] = useState(false); const [loading, setLoading] = useState(false); @@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) { }); return ( - + <> {isI18nReady @@ -195,9 +184,11 @@ export default function App(props: EnteAppProps) { <EnteSpinner /> </Overlay> )} - <Component setLoading={setLoading} {...pageProps} /> + {isI18nReady && ( + <Component setLoading={setLoading} {...pageProps} /> + )} </AppContext.Provider> </ThemeProvider> - </CacheProvider> + </> ); } diff --git a/web/apps/auth/src/pages/_document.tsx b/web/apps/auth/src/pages/_document.tsx index 09d4d5782..3c6c2a959 100644 --- a/web/apps/auth/src/pages/_document.tsx +++ b/web/apps/auth/src/pages/_document.tsx @@ -1,7 +1,3 @@ -import DocumentPage, { - EnteDocumentProps, -} from "@ente/shared/next/pages/_document"; +import DocumentPage from "@ente/shared/next/pages/_document"; -export default function Document(props: EnteDocumentProps) { - return <DocumentPage {...props} />; -} +export default DocumentPage; diff --git a/web/apps/cast/public/locales/bg-BG/translation.json b/web/apps/cast/public/locales/bg-BG/translation.json new file mode 100644 index 000000000..03faf16c2 --- /dev/null +++ b/web/apps/cast/public/locales/bg-BG/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Личен бекъп</div><div>на твоите спомени</div>", + "HERO_SLIDE_1": "Криптиран от край до край по подразбиране", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/de-DE/translation.json b/web/apps/cast/public/locales/de-DE/translation.json new file mode 100644 index 000000000..9a1c4073c --- /dev/null +++ b/web/apps/cast/public/locales/de-DE/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Private Sicherungen</div><div>für deine Erinnerungen</div>", + "HERO_SLIDE_1": "Standardmäßig Ende-zu-Ende verschlüsselt", + "HERO_SLIDE_2_TITLE": "<div>Sicher gespeichert</div><div>in einem Luftschutzbunker</div>", + "HERO_SLIDE_2": "Entwickelt um zu bewahren", + "HERO_SLIDE_3_TITLE": "<div>Verfügbar</div><div> überall</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Desktop", + "LOGIN": "Anmelden", + "SIGN_UP": "Registrieren", + "NEW_USER": "Neu bei ente", + "EXISTING_USER": "Existierender Benutzer", + "ENTER_NAME": "Name eingeben", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Füge einen Namen hinzu, damit deine Freunde wissen, wem sie für diese tollen Fotos zu danken haben!", + "ENTER_EMAIL": "E-Mail-Adresse eingeben", + "EMAIL_ERROR": "Geben Sie eine gültige E-Mail-Adresse ein", + "REQUIRED": "Erforderlich", + "EMAIL_SENT": "Bestätigungscode an <a>{{email}}</a> gesendet", + "CHECK_INBOX": "Bitte überprüfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen", + "ENTER_OTT": "Bestätigungscode", + "RESEND_MAIL": "Code erneut senden", + "VERIFY": "Überprüfen", + "UNKNOWN_ERROR": "Ein Fehler ist aufgetreten, bitte versuche es erneut", + "INVALID_CODE": "Falscher Bestätigungscode", + "EXPIRED_CODE": "Ihr Bestätigungscode ist abgelaufen", + "SENDING": "Wird gesendet...", + "SENT": "Gesendet!", + "PASSWORD": "Passwort", + "LINK_PASSWORD": "Passwort zum Entsperren des Albums eingeben", + "RETURN_PASSPHRASE_HINT": "Passwort", + "SET_PASSPHRASE": "Passwort setzen", + "VERIFY_PASSPHRASE": "Einloggen", + "INCORRECT_PASSPHRASE": "Falsches Passwort", + "ENTER_ENC_PASSPHRASE": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", + "PASSPHRASE_DISCLAIMER": "Wir speichern dein Passwort nicht. Wenn du es vergisst, <strong>können wir dir nicht helfen</strong>, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", + "WELCOME_TO_ENTE_HEADING": "Willkommen bei <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Wo deine besten Fotos leben", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...", + "PASSPHRASE_HINT": "Passwort", + "CONFIRM_PASSPHRASE": "Passwort bestätigen", + "REFERRAL_CODE_HINT": "Wie hast du von Ente erfahren? (optional)", + "REFERRAL_INFO": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", + "PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein", + "CREATE_COLLECTION": "Neues Album", + "ENTER_ALBUM_NAME": "Albumname", + "CLOSE_OPTION": "Schließen (Esc)", + "ENTER_FILE_NAME": "Dateiname", + "CLOSE": "Schließen", + "NO": "Nein", + "NOTHING_HERE": "Hier gibt es noch nichts zu sehen 👀", + "UPLOAD": "Hochladen", + "IMPORT": "Importieren", + "ADD_PHOTOS": "Fotos hinzufügen", + "ADD_MORE_PHOTOS": "Mehr Fotos hinzufügen", + "add_photos_one": "Eine Datei hinzufügen", + "add_photos_other": "{{count, number}} Dateien hinzufügen", + "SELECT_PHOTOS": "Foto auswählen", + "FILE_UPLOAD": "Datei hochladen", + "UPLOAD_STAGE_MESSAGE": { + "0": "Hochladen wird vorbereitet", + "1": "Lese Google-Metadaten", + "2": "Metadaten von {{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien extrahiert", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien verarbeitet", + "4": "Verbleibende Uploads werden abgebrochen", + "5": "Sicherung abgeschlossen" + }, + "FILE_NOT_UPLOADED_LIST": "Die folgenden Dateien wurden nicht hochgeladen", + "SUBSCRIPTION_EXPIRED": "Abonnement abgelaufen", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Dein Abonnement ist abgelaufen, bitte <a>erneuere es</a>", + "STORAGE_QUOTA_EXCEEDED": "Speichergrenze überschritten", + "INITIAL_LOAD_DELAY_WARNING": "Das erste Laden kann einige Zeit in Anspruch nehmen", + "USER_DOES_NOT_EXIST": "Leider konnte kein Benutzer mit dieser E-Mail gefunden werden", + "NO_ACCOUNT": "Kein Konto vorhanden", + "ACCOUNT_EXISTS": "Es ist bereits ein Account vorhanden", + "CREATE": "Erstellen", + "DOWNLOAD": "Herunterladen", + "DOWNLOAD_OPTION": "Herunterladen (D)", + "DOWNLOAD_FAVORITES": "Favoriten herunterladen", + "DOWNLOAD_UNCATEGORIZED": "Download unkategorisiert", + "DOWNLOAD_HIDDEN_ITEMS": "Versteckte Dateien herunterladen", + "COPY_OPTION": "Als PNG kopieren (Strg / Cmd - C)", + "TOGGLE_FULLSCREEN": "Vollbild umschalten (F)", + "ZOOM_IN_OUT": "Herein-/Herauszoomen", + "PREVIOUS": "Vorherige (←)", + "NEXT": "Weitere (→)", + "TITLE_PHOTOS": "Ente Fotos", + "TITLE_ALBUMS": "Ente Fotos", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "Lade dein erstes Foto hoch", + "IMPORT_YOUR_FOLDERS": "Importiere deiner Ordner", + "UPLOAD_DROPZONE_MESSAGE": "Loslassen, um Dateien zu sichern", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Loslassen, um beobachteten Ordner hinzuzufügen", + "TRASH_FILES_TITLE": "Dateien löschen?", + "TRASH_FILE_TITLE": "Datei löschen?", + "DELETE_FILES_TITLE": "Sofort löschen?", + "DELETE_FILES_MESSAGE": "Ausgewählte Dateien werden dauerhaft aus Ihrem Ente-Konto gelöscht.", + "DELETE": "Löschen", + "DELETE_OPTION": "Löschen (DEL)", + "FAVORITE_OPTION": "Zu Favoriten hinzufügen (L)", + "UNFAVORITE_OPTION": "Von Favoriten entfernen (L)", + "MULTI_FOLDER_UPLOAD": "Mehrere Ordner erkannt", + "UPLOAD_STRATEGY_CHOICE": "Möchtest du sie hochladen in", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Ein einzelnes Album", + "OR": "oder", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Getrennte Alben", + "SESSION_EXPIRED_MESSAGE": "Ihre Sitzung ist abgelaufen. Bitte loggen Sie sich erneut ein, um fortzufahren", + "SESSION_EXPIRED": "Sitzung abgelaufen", + "PASSWORD_GENERATION_FAILED": "Dein Browser konnte keinen starken Schlüssel generieren, der den Verschlüsselungsstandards des Entes entspricht, bitte versuche die mobile App oder einen anderen Browser zu verwenden", + "CHANGE_PASSWORD": "Passwort ändern", + "GO_BACK": "Zurück", + "RECOVERY_KEY": "Wiederherstellungsschlüssel", + "SAVE_LATER": "Auf später verschieben", + "SAVE": "Schlüssel speichern", + "RECOVERY_KEY_DESCRIPTION": "Falls du dein Passwort vergisst, kannst du deine Daten nur mit diesem Schlüssel wiederherstellen.", + "RECOVER_KEY_GENERATION_FAILED": "Wiederherstellungsschlüssel konnte nicht generiert werden, bitte versuche es erneut", + "KEY_NOT_STORED_DISCLAIMER": "Wir speichern diesen Schlüssel nicht, also speichere ihn bitte an einem sicheren Ort", + "FORGOT_PASSWORD": "Passwort vergessen", + "RECOVER_ACCOUNT": "Konto wiederherstellen", + "RECOVERY_KEY_HINT": "Wiederherstellungsschlüssel", + "RECOVER": "Wiederherstellen", + "NO_RECOVERY_KEY": "Kein Wiederherstellungsschlüssel?", + "INCORRECT_RECOVERY_KEY": "Falscher Wiederherstellungs-Schlüssel", + "SORRY": "Entschuldigung", + "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 <a>{{emailID}}</a> von deiner registrierten E-Mail-Adresse", + "CONTACT_SUPPORT": "Support kontaktieren", + "REQUEST_FEATURE": "Feature anfragen", + "SUPPORT": "Support", + "CONFIRM": "Bestätigen", + "CANCEL": "Abbrechen", + "LOGOUT": "Ausloggen", + "DELETE_ACCOUNT": "Konto löschen", + "DELETE_ACCOUNT_MESSAGE": "<p>Bitte sende eine E-Mail an <a>{{emailID}}</a> mit deiner registrierten E-Mail-Adresse.</p><p>Deine Anfrage wird innerhalb von 72 Stunden bearbeitet.</p>", + "LOGOUT_MESSAGE": "Sind sie sicher, dass sie sich ausloggen möchten?", + "CHANGE_EMAIL": "E-Mail-Adresse ändern", + "OK": "OK", + "SUCCESS": "Erfolgreich", + "ERROR": "Fehler", + "MESSAGE": "Nachricht", + "INSTALL_MOBILE_APP": "Installiere unsere <a>Android</a> oder <b>iOS</b> App, um automatisch alle deine Fotos zu sichern", + "DOWNLOAD_APP_MESSAGE": "Entschuldigung, dieser Vorgang wird derzeit nur von unserer Desktop-App unterstützt", + "DOWNLOAD_APP": "Desktopanwendung herunterladen", + "EXPORT": "Daten exportieren", + "SUBSCRIPTION": "Abonnement", + "SUBSCRIBE": "Abonnieren", + "MANAGEMENT_PORTAL": "Zahlungsmethode verwalten", + "MANAGE_FAMILY_PORTAL": "Familiengruppe verwalten", + "LEAVE_FAMILY_PLAN": "Familienabo verlassen", + "LEAVE": "Verlassen", + "LEAVE_FAMILY_CONFIRM": "Bist du sicher, dass du den Familien-Tarif verlassen möchtest?", + "CHOOSE_PLAN": "Wähle dein Abonnement", + "MANAGE_PLAN": "Verwalte dein Abonnement", + "ACTIVE": "Aktiv", + "OFFLINE_MSG": "Du bist offline, gecachte Erinnerungen werden angezeigt", + "FREE_SUBSCRIPTION_INFO": "Du bist auf dem <strong>kostenlosen</strong> Plan, der am {{date, dateTime}} ausläuft", + "FAMILY_SUBSCRIPTION_INFO": "Sie haben einen Familienplan verwaltet von", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Erneuert am {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Endet am {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Ihr Abo endet am {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Dein {{storage, string}} Add-on ist gültig bis {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Sie haben Ihr Speichervolumen überschritten, bitte <a>upgraden Sie</a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>Wir haben deine Zahlung erhalten</p><p>Dein Abonnement ist gültig bis <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Dein Kauf wurde abgebrochen. Bitte versuche es erneut, wenn du abonnieren willst", + "SUBSCRIPTION_PURCHASE_FAILED": "Kauf des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", + "SUBSCRIPTION_UPDATE_FAILED": "Aktualisierung des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Es tut uns leid, die Zahlung ist fehlgeschlagen, als wir versuchten Ihre Karte zu belasten. Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut", + "STRIPE_AUTHENTICATION_FAILED": "Wir können deine Zahlungsmethode nicht authentifizieren. Bitte wähle eine andere Zahlungsmethode und versuche es erneut", + "UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren", + "MONTHLY": "Monatlich", + "YEARLY": "Jährlich", + "UPDATE_SUBSCRIPTION_MESSAGE": "Sind Sie sicher, dass Sie Ihren Tarif ändern möchten?", + "UPDATE_SUBSCRIPTION": "Plan ändern", + "CANCEL_SUBSCRIPTION": "Abonnement kündigen", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Alle deine Daten werden am Ende dieses Abrechnungszeitraums von unseren Servern gelöscht.</p><p>Bist du sicher, dass du dein Abonnement kündigen möchtest?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Bist du sicher, dass du dein Abonnement beenden möchtest?</p>", + "SUBSCRIPTION_CANCEL_FAILED": "Abonnement konnte nicht storniert werden", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement erfolgreich beendet", + "REACTIVATE_SUBSCRIPTION": "Abonnement reaktivieren", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Nach der Reaktivierung wird am {{date, dateTime}} abgerechnet", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Abonnement erfolgreich aktiviert ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Reaktivierung der Abonnementverlängerung fehlgeschlagen", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Vielen Dank", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Mobiles Abonnement kündigen", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Bitte kündige dein Abonnement in der mobilen App, um hier ein Abonnement zu aktivieren", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Bitte kontaktiere uns über <a>{{emailID}}</a>, um dein Abo zu verwalten", + "RENAME": "Umbenennen", + "RENAME_FILE": "Datei umbenennen", + "RENAME_COLLECTION": "Album umbenennen", + "DELETE_COLLECTION_TITLE": "Album löschen?", + "DELETE_COLLECTION": "Album löschen", + "DELETE_COLLECTION_MESSAGE": "Auch die Fotos (und Videos) in diesem Album aus <a>allen</a> anderen Alben löschen, die sie enthalten?", + "DELETE_PHOTOS": "Fotos löschen", + "KEEP_PHOTOS": "Fotos behalten", + "SHARE": "Teilen", + "SHARE_COLLECTION": "Album teilen", + "SHAREES": "Geteilt mit", + "SHARE_WITH_SELF": "Du kannst nicht mit dir selbst teilen", + "ALREADY_SHARED": "Hoppla, Sie teilen dies bereits mit {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Albumfreigabe nicht erlaubt", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Freigabe ist für kostenlose Konten deaktiviert", + "DOWNLOAD_COLLECTION": "Album herunterladen", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Bist du sicher, dass du das komplette Album herunterladen möchtest?</p><p>Alle Dateien werden der Warteschlange zum sequenziellen Download hinzugefügt</p>", + "CREATE_ALBUM_FAILED": "Fehler beim Erstellen des Albums, bitte versuche es erneut", + "SEARCH": "Suchen", + "SEARCH_RESULTS": "Ergebnisse durchsuchen", + "NO_RESULTS": "Keine Ergebnisse gefunden", + "SEARCH_HINT": "Suche nach Alben, Datum, Beschreibungen, ...", + "SEARCH_TYPE": { + "COLLECTION": "Album", + "LOCATION": "Standort", + "CITY": "Ort", + "DATE": "Datum", + "FILE_NAME": "Dateiname", + "THING": "Inhalt", + "FILE_CAPTION": "Beschreibung", + "FILE_TYPE": "Dateityp", + "CLIP": "Magie" + }, + "photos_count_zero": "Keine Erinnerungen", + "photos_count_one": "Eine Erinnerung", + "photos_count_other": "{{count, number}} Erinnerungen", + "TERMS_AND_CONDITIONS": "Ich stimme den <a>Bedingungen</a> und <b>Datenschutzrichtlinien</b> zu", + "ADD_TO_COLLECTION": "Zum Album hinzufügen", + "SELECTED": "ausgewählt", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Dieses Video kann in deinem Browser nicht abgespielt werden", + "PEOPLE": "Personen", + "INDEXING_SCHEDULED": "Indizierung ist geplant...", + "ANALYZING_PHOTOS": "Indiziere Fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Indiziere Personen in {{indexStatus.nSyncedFiles,number}} Fotos...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles,number}} Fotos wurden indiziert", + "UNIDENTIFIED_FACES": "unidentifizierte Gesichter", + "OBJECTS": "Objekte", + "TEXT": "Text", + "INFO": "Info ", + "INFO_OPTION": "Info (I)", + "FILE_NAME": "Dateiname", + "CAPTION_PLACEHOLDER": "Eine Beschreibung hinzufügen", + "LOCATION": "Standort", + "SHOW_ON_MAP": "In OpenStreetMap öffnen", + "MAP": "Karte", + "MAP_SETTINGS": "Karten\nEinstellungen", + "ENABLE_MAPS": "Karten aktivieren?", + "ENABLE_MAP": "Karte aktivieren", + "DISABLE_MAPS": "Karten deaktivieren?", + "ENABLE_MAP_DESCRIPTION": "<p>Dies wird deine Fotos auf einer Weltkarte anzeigen.</p> <p>Die Karte wird von <a>OpenStreetMap</a> gehostet und die genauen Standorte deiner Fotos werden niemals geteilt.</p> <p>Diese Funktion kannst du jederzeit in den Einstellungen deaktivieren.</p>", + "DISABLE_MAP_DESCRIPTION": "<p>Dies wird die Anzeige deiner Fotos auf einer Weltkarte deaktivieren.</p> <p>Du kannst diese Funktion jederzeit in den Einstellungen aktivieren.</p>", + "DISABLE_MAP": "Karte deaktivieren", + "DETAILS": "Details", + "VIEW_EXIF": "Alle EXIF-Daten anzeigen", + "NO_EXIF": "Keine EXIF-Daten", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Zwei-Faktor", + "TWO_FACTOR_AUTHENTICATION": "Zwei-Faktor-Authentifizierung", + "TWO_FACTOR_QR_INSTRUCTION": "Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App", + "ENTER_CODE_MANUALLY": "Geben Sie den Code manuell ein", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Bitte gib diesen Code in deiner bevorzugten Authentifizierungs-App ein", + "SCAN_QR_CODE": "QR‐Code stattdessen scannen", + "ENABLE_TWO_FACTOR": "Zwei-Faktor-Authentifizierung aktivieren", + "ENABLE": "Aktivieren", + "LOST_DEVICE": "Zwei-Faktor-Gerät verloren", + "INCORRECT_CODE": "Falscher Code", + "TWO_FACTOR_INFO": "Fügen Sie eine zusätzliche Sicherheitsebene hinzu, indem Sie mehr als Ihre E-Mail und Ihr Passwort benötigen, um sich mit Ihrem Account anzumelden", + "DISABLE_TWO_FACTOR_LABEL": "Deaktiviere die Zwei-Faktor-Authentifizierung", + "UPDATE_TWO_FACTOR_LABEL": "Authentifizierungsgerät aktualisieren", + "DISABLE": "Deaktivieren", + "RECONFIGURE": "Neu einrichten", + "UPDATE_TWO_FACTOR": "Zweiten Faktor aktualisieren", + "UPDATE_TWO_FACTOR_MESSAGE": "Fahren Sie fort, werden alle Ihre zuvor konfigurierten Authentifikatoren ungültig", + "UPDATE": "Aktualisierung", + "DISABLE_TWO_FACTOR": "Zweiten Faktor deaktivieren", + "DISABLE_TWO_FACTOR_MESSAGE": "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst", + "TWO_FACTOR_DISABLE_FAILED": "Fehler beim Deaktivieren des zweiten Faktors, bitte versuchen Sie es erneut", + "EXPORT_DATA": "Daten exportieren", + "SELECT_FOLDER": "Ordner auswählen", + "DESTINATION": "Zielort", + "START": "Start", + "LAST_EXPORT_TIME": "Letztes Exportdatum", + "EXPORT_AGAIN": "Neusynchronisation", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", + "SEND_OTT": "OTP senden", + "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "Fehlgeschlagene Uploads erneut probieren", + "FAILED_UPLOADS": "Fehlgeschlagene Uploads ", + "SKIPPED_FILES": "Ignorierte Uploads", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Das Vorschaubild konnte nicht erzeugt werden", + "UNSUPPORTED_FILES": "Nicht unterstützte Dateien", + "SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "ente unterstützt diese Dateiformate noch nicht", + "BLOCKED_UPLOADS": "Blockierte Uploads", + "SKIPPED_VIDEOS": "Übersprungene Videos", + "INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung", + "INPROGRESS_UPLOADS": "Upload läuft", + "TOO_LARGE_UPLOADS": "Große Dateien", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Zu wenig Speicher", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie die maximale Größe für Ihren Speicherplan überschreiten", + "TOO_LARGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", + "THUMBNAIL_GENERATION_FAILED_INFO": "Diese Dateien wurden hochgeladen, aber leider konnten wir nicht die Thumbnails für sie generieren.", + "UPLOAD_TO_COLLECTION": "In Album hochladen", + "UNCATEGORIZED": "Unkategorisiert", + "ARCHIVE": "Archiv", + "FAVORITES": "Favoriten", + "ARCHIVE_COLLECTION": "Album archivieren", + "ARCHIVE_SECTION_NAME": "Archiv", + "ALL_SECTION_NAME": "Alle", + "MOVE_TO_COLLECTION": "Zum Album verschieben", + "UNARCHIVE": "Dearchivieren", + "UNARCHIVE_COLLECTION": "Album dearchivieren", + "HIDE_COLLECTION": "Album ausblenden", + "UNHIDE_COLLECTION": "Album wieder einblenden", + "MOVE": "Verschieben", + "ADD": "Hinzufügen", + "REMOVE": "Entfernen", + "YES_REMOVE": "Ja, entfernen", + "REMOVE_FROM_COLLECTION": "Aus Album entfernen", + "TRASH": "Papierkorb", + "MOVE_TO_TRASH": "In Papierkorb verschieben", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "Dauerhaft löschen", + "RESTORE": "Wiederherstellen", + "RESTORE_TO_COLLECTION": "In Album wiederherstellen", + "EMPTY_TRASH": "Papierkorb leeren", + "EMPTY_TRASH_TITLE": "Papierkorb leeren?", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "Ja, verlassen", + "LEAVE_ALBUM": "Album verlassen", + "LEAVE_SHARED_ALBUM_TITLE": "Geteiltes Album verlassen?", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "Dateien in einem freigegebenen Album können nicht gelöscht werden", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", + "SORT_BY_CREATION_TIME_ASCENDING": "Ältestem", + "SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert", + "SORT_BY_NAME": "Name", + "COMPRESS_THUMBNAILS": "Vorschaubilder komprimieren", + "THUMBNAIL_REPLACED": "Vorschaubilder komprimiert", + "FIX_THUMBNAIL": "Komprimiere", + "FIX_THUMBNAIL_LATER": "Später komprimieren", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "Zeit reparieren", + "FIX_CREATION_TIME_IN_PROGRESS": "Zeit wird repariert", + "CREATION_TIME_UPDATED": "Datei-Zeit aktualisiert", + "UPDATE_CREATION_TIME_NOT_STARTED": "Wählen Sie die Option, die Sie verwenden möchten", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "Benutzerdefinierte Zeit", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Fehler beim Öffnen der Pläne", + "INSTALL": "Installieren", + "SHARING_DETAILS": "Details teilen", + "MODIFY_SHARING": "Freigabe ändern", + "ADD_COLLABORATORS": "Bearbeiter hinzufügen", + "ADD_NEW_EMAIL": "Neue E-Mail-Adresse hinzufügen", + "shared_with_people_zero": "Mit bestimmten Personen teilen", + "shared_with_people_one": "Geteilt mit einer Person", + "shared_with_people_other": "Geteilt mit {{count, number}} Personen", + "participants_zero": "Keine Teilnehmer", + "participants_one": "1 Teilnehmer", + "participants_other": "{{count, number}} Teilnehmer", + "ADD_VIEWERS": "Betrachter hinzufügen", + "PARTICIPANTS": "Teilnehmer", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "Berechtigung ändern?", + "REMOVE_PARTICIPANT": "Entfernen?", + "CONFIRM_REMOVE": "Ja, entfernen", + "MANAGE": "Verwalten", + "ADDED_AS": "Hinzugefügt als", + "COLLABORATOR_RIGHTS": "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen", + "REMOVE_PARTICIPANT_HEAD": "Teilnehmer entfernen", + "OWNER": "Besitzer", + "COLLABORATORS": "Bearbeiter", + "ADD_MORE": "Mehr hinzufügen", + "VIEWERS": "Zuschauer", + "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "404 - Nicht gefunden", + "LINK_EXPIRED": "Link ist abgelaufen", + "LINK_EXPIRED_MESSAGE": "Dieser Link ist abgelaufen oder wurde deaktiviert!", + "MANAGE_LINK": "Link verwalten", + "LINK_TOO_MANY_REQUESTS": "Sorry, dieses Album wurde auf zu vielen Geräten angezeigt!", + "FILE_DOWNLOAD": "Downloads erlauben", + "LINK_PASSWORD_LOCK": "Passwort Sperre", + "PUBLIC_COLLECT": "Hinzufügen von Fotos erlauben", + "LINK_DEVICE_LIMIT": "Geräte Limit", + "NO_DEVICE_LIMIT": "Keins", + "LINK_EXPIRY": "Ablaufdatum des Links", + "NEVER": "Niemals", + "DISABLE_FILE_DOWNLOAD": "Download deaktivieren", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "Enthält schädliche Inhalte", + "COPYRIGHT": "Verletzung des Urheberrechts von jemandem, den ich repräsentieren darf", + "SHARED_USING": "Freigegeben über ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "", + "LIVE": "LIVE", + "DISABLE_PASSWORD": "Passwort-Sperre deaktivieren", + "DISABLE_PASSWORD_MESSAGE": "Sind Sie sicher, dass Sie die Passwort-Sperre deaktivieren möchten?", + "PASSWORD_LOCK": "Passwort Sperre", + "LOCK": "Sperren", + "DOWNLOAD_UPLOAD_LOGS": "Debug-Logs", + "UPLOAD_FILES": "Datei", + "UPLOAD_DIRS": "Ordner", + "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "Authenticator", + "NO_DUPLICATES_FOUND": "Du hast keine Duplikate, die gelöscht werden können", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "Dateien", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "Hochladen stoppen?", + "YES_STOP_UPLOADS": "Ja, Hochladen stoppen", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "1 Album", + "albums_other": "", + "ALL_ALBUMS": "Alle Alben", + "ALBUMS": "Alben", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "Gib den 6-stelligen Code aus\ndeiner Authentifizierungs-App ein.", + "CREATE_ACCOUNT": "Account erstellen", + "COPIED": "Kopiert", + "CANVAS_BLOCKED_TITLE": "Vorschaubild konnte nicht erstellt werden", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "Jetzt upgraden", + "RENEW_NOW": "", + "STORAGE": "Speicher", + "USED": "verwendet", + "YOU": "Sie", + "FAMILY": "Familie", + "FREE": "frei", + "OF": "von", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "Ordner hinzufügen", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "Ja, Stopp", + "MONTH_SHORT": "", + "YEAR": "Jahr", + "FAMILY_PLAN": "Familientarif", + "DOWNLOAD_LOGS": "Logs herunterladen", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "Ordner ändern", + "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", + "GB": "GB", + "POPULAR": "Beliebt", + "FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren", + "FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr", + "CURRENT_USAGE": "Aktuelle Nutzung ist <strong>{{usage}}</strong>", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "AUTHENTICATE": "Authentifizieren", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "NEVERMIND": "Egal", + "UPDATE_AVAILABLE": "Neue Version verfügbar", + "UPDATE_INSTALLABLE_MESSAGE": "", + "INSTALL_NOW": "Jetzt installieren", + "INSTALL_ON_NEXT_LAUNCH": "Beim nächsten Start installieren", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", + "IGNORE_THIS_VERSION": "Diese Version ignorieren", + "TODAY": "Heute", + "YESTERDAY": "Gestern", + "NAME_PLACEHOLDER": "Name...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "Beta deaktivieren", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "Erweitert", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "Passwortstärke: Schwach", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "Passwortstärke: Stark", + "PREFERENCES": "Einstellungen", + "LANGUAGE": "Sprache", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "nach einer Stunde", + "DAY": "nach einem Tag", + "WEEK": "nach 1 Woche", + "MONTH": "nach einem Monat", + "YEAR": "nach einem Jahr" + }, + "COPY_LINK": "Link kopieren", + "DONE": "Fertig", + "LINK_SHARE_TITLE": "Oder einen Link teilen", + "REMOVE_LINK": "Link entfernen", + "CREATE_PUBLIC_SHARING": "Öffentlichen Link erstellen", + "PUBLIC_LINK_CREATED": "Öffentlicher Link erstellt", + "PUBLIC_LINK_ENABLED": "Öffentlicher Link aktiviert", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "Stop", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "Export gestartet", + "IN_PROGRESS": "", + "FINISH": "Export abgeschlossen", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "Kontolöschung bestätigen", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "Weiter", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "Versteckt", + "HIDE": "Ausblenden", + "UNHIDE": "Einblenden", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "Sortieren nach", + "NEWEST_FIRST": "Neueste zuerst", + "OLDEST_FIRST": "Älteste zuerst", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Diese Datei konnte nicht in der Vorschau angezeigt werden. Klicken Sie hier, um das Original herunterzuladen.", + "SELECT_COLLECTION": "Album auswählen", + "PIN_ALBUM": "Album anheften", + "UNPIN_ALBUM": "Album lösen", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/accounts/public/locales/en/translation.json b/web/apps/cast/public/locales/en-US/translation.json similarity index 97% rename from web/apps/accounts/public/locales/en/translation.json rename to web/apps/cast/public/locales/en-US/translation.json index 6870df319..de8d2fe2a 100644 --- a/web/apps/accounts/public/locales/en/translation.json +++ b/web/apps/cast/public/locales/en-US/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", "CREATE_COLLECTION": "New album", "ENTER_ALBUM_NAME": "Album name", "CLOSE_OPTION": "Close (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Downloading {{name}}", "DOWNLOAD_FAILED": "Download failed", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", "CHRISTMAS": "Christmas", "CHRISTMAS_EVE": "Christmas Eve", "NEW_YEAR": "New Year", @@ -637,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", "FREEHAND": "Freehand", "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.", + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Delete passkey", + "DELETE_PASSKEY_CONFIRMATION": "Are you sure you want to delete this passkey? This action is irreversible.", + "RENAME_PASSKEY": "Rename passkey", + "ADD_PASSKEY": "Add passkey", + "ENTER_PASSKEY_NAME": "Enter passkey name", + "PASSKEYS_DESCRIPTION": "Passkeys are a modern and secure second-factor for your Ente account. They use on-device biometric authentication for convenience and security.", + "CREATED_AT": "Created at", + "PASSKEY_LOGIN_FAILED": "Passkey login failed", + "PASSKEY_LOGIN_URL_INVALID": "The login URL is invalid.", + "PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.", + "TRY_AGAIN": "Try again", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.", + "LOGIN_WITH_PASSKEY": "Login with passkey" } diff --git a/web/apps/cast/public/locales/es-ES/translation.json b/web/apps/cast/public/locales/es-ES/translation.json new file mode 100644 index 000000000..a29165e4e --- /dev/null +++ b/web/apps/cast/public/locales/es-ES/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Copias de seguridad privadas</div><div>para su recuerdos</div>", + "HERO_SLIDE_1": "Encriptado de extremo a extremo por defecto", + "HERO_SLIDE_2_TITLE": "<div>Almacenado de forma segura</div><div>en un refugio de llenos</div>", + "HERO_SLIDE_2": "Diseñado para superar", + "HERO_SLIDE_3_TITLE": "<div>Disponible</div><div> en todas partes</div>", + "HERO_SLIDE_3": "Android, iOS, web, computadora", + "LOGIN": "Conectar", + "SIGN_UP": "Registro", + "NEW_USER": "Nuevo en ente", + "EXISTING_USER": "Usuario existente", + "ENTER_NAME": "Introducir nombre", + "PUBLIC_UPLOADER_NAME_MESSAGE": "¡Añade un nombre para que tus amigos sepan a quién dar las gracias por estas fotos geniales!", + "ENTER_EMAIL": "Introducir email", + "EMAIL_ERROR": "Introduce un email válido", + "REQUIRED": "Requerido", + "EMAIL_SENT": "Código de verificación enviado al <a>{{email}}</a>", + "CHECK_INBOX": "Revisa tu bandeja de entrada (y spam) para completar la verificación", + "ENTER_OTT": "Código de verificación", + "RESEND_MAIL": "Reenviar el código", + "VERIFY": "Verificar", + "UNKNOWN_ERROR": "Se produjo un error. Por favor, inténtalo de nuevo", + "INVALID_CODE": "Código de verificación inválido", + "EXPIRED_CODE": "Código de verificación expirado", + "SENDING": "Enviando...", + "SENT": "Enviado!", + "PASSWORD": "Contraseña", + "LINK_PASSWORD": "Introducir contraseña para desbloquear el álbum", + "RETURN_PASSPHRASE_HINT": "Contraseña", + "SET_PASSPHRASE": "Definir contraseña", + "VERIFY_PASSPHRASE": "Ingresar", + "INCORRECT_PASSPHRASE": "Contraseña incorrecta", + "ENTER_ENC_PASSPHRASE": "Introducir una contraseña que podamos usar para cifrar sus datos", + "PASSPHRASE_DISCLAIMER": "No guardamos su contraseña, así que si la olvida, <strong>no podremos ayudarte </strong>a recuperar tus datos sin una clave de recuperación.", + "WELCOME_TO_ENTE_HEADING": "Bienvenido a <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Almacenamiento y compartición de fotos cifradas de extremo a extremo", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Donde vivan su mejores fotos", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generando claves de encriptación...", + "PASSPHRASE_HINT": "Contraseña", + "CONFIRM_PASSPHRASE": "Confirmar contraseña", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden", + "CREATE_COLLECTION": "Nuevo álbum", + "ENTER_ALBUM_NAME": "Nombre del álbum", + "CLOSE_OPTION": "Cerrar (Esc)", + "ENTER_FILE_NAME": "Nombre del archivo", + "CLOSE": "Cerrar", + "NO": "No", + "NOTHING_HERE": "Nada para ver aquí aún 👀", + "UPLOAD": "Cargar", + "IMPORT": "Importar", + "ADD_PHOTOS": "Añadir fotos", + "ADD_MORE_PHOTOS": "Añadir más fotos", + "add_photos_one": "Añadir 1 foto", + "add_photos_other": "Añadir {{count}} fotos", + "SELECT_PHOTOS": "Seleccionar fotos", + "FILE_UPLOAD": "Subir archivo", + "UPLOAD_STAGE_MESSAGE": { + "0": "Preparando la subida", + "1": "Leyendo archivos de metadatos de google", + "2": "{{uploadCounter.finished}} / {{uploadCounter.total}} archivos metadatos extraídos", + "3": "{{uploadCounter.finished}} / {{uploadCounter.total}} archivos metadatos extraídos", + "4": "Cancelar subidas restantes", + "5": "Copia de seguridad completa" + }, + "FILE_NOT_UPLOADED_LIST": "Los siguientes archivos no se han subido", + "SUBSCRIPTION_EXPIRED": "Suscripción caducada", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Tu suscripción ha caducado, por favor <a>renuévala</a>", + "STORAGE_QUOTA_EXCEEDED": "Límite de datos excedido", + "INITIAL_LOAD_DELAY_WARNING": "La primera carga puede tomar algún tiempo", + "USER_DOES_NOT_EXIST": "Lo sentimos, no se pudo encontrar un usuario con ese email", + "NO_ACCOUNT": "No tienes una cuenta", + "ACCOUNT_EXISTS": "Ya tienes una cuenta", + "CREATE": "Crear", + "DOWNLOAD": "Descargar", + "DOWNLOAD_OPTION": "Descargar (D)", + "DOWNLOAD_FAVORITES": "Descargar favoritos", + "DOWNLOAD_UNCATEGORIZED": "Descargar no categorizados", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "Copiar como PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Alternar pantalla completa (F)", + "ZOOM_IN_OUT": "Acercar/alejar", + "PREVIOUS": "Anterior (←)", + "NEXT": "Siguiente (→)", + "TITLE_PHOTOS": "ente Fotos", + "TITLE_ALBUMS": "ente Fotos", + "TITLE_AUTH": "ente Auth", + "UPLOAD_FIRST_PHOTO": "Carga tu primer archivo", + "IMPORT_YOUR_FOLDERS": "Importar tus carpetas", + "UPLOAD_DROPZONE_MESSAGE": "Soltar para respaldar tus archivos", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Soltar para añadir carpeta vigilada", + "TRASH_FILES_TITLE": "Eliminar archivos?", + "TRASH_FILE_TITLE": "Eliminar archivo?", + "DELETE_FILES_TITLE": "Eliminar inmediatamente?", + "DELETE_FILES_MESSAGE": "Los archivos seleccionados serán eliminados permanentemente de tu cuenta ente.", + "DELETE": "Eliminar", + "DELETE_OPTION": "Eliminar (DEL)", + "FAVORITE_OPTION": "Favorito (L)", + "UNFAVORITE_OPTION": "No favorito (L)", + "MULTI_FOLDER_UPLOAD": "Múltiples carpetas detectadas", + "UPLOAD_STRATEGY_CHOICE": "Quieres subirlos a", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Un solo álbum", + "OR": "o", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separar álbumes", + "SESSION_EXPIRED_MESSAGE": "Tu sesión ha caducado. Inicia sesión de nuevo para continuar", + "SESSION_EXPIRED": "Sesión caducado", + "PASSWORD_GENERATION_FAILED": "Su navegador no ha podido generar una clave fuerte que cumpla con los estándares de cifrado de la entidad, por favor intente usar la aplicación móvil u otro navegador", + "CHANGE_PASSWORD": "Cambiar contraseña", + "GO_BACK": "Retroceder", + "RECOVERY_KEY": "Clave de recuperación", + "SAVE_LATER": "Hacer más tarde", + "SAVE": "Guardar Clave", + "RECOVERY_KEY_DESCRIPTION": "Si olvida su contraseña, la única forma de recuperar sus datos es con esta clave.", + "RECOVER_KEY_GENERATION_FAILED": "El código de recuperación no pudo ser generado, por favor inténtalo de nuevo", + "KEY_NOT_STORED_DISCLAIMER": "No almacenamos esta clave, así que por favor guarde esto en un lugar seguro", + "FORGOT_PASSWORD": "Contraseña olvidada", + "RECOVER_ACCOUNT": "Recuperar cuenta", + "RECOVERY_KEY_HINT": "Clave de recuperación", + "RECOVER": "Recuperar", + "NO_RECOVERY_KEY": "No hay clave de recuperación?", + "INCORRECT_RECOVERY_KEY": "Clave de recuperación incorrecta", + "SORRY": "Lo sentimos", + "NO_RECOVERY_KEY_MESSAGE": "Debido a la naturaleza de nuestro protocolo de cifrado de extremo a extremo, sus datos no pueden ser descifrados sin su contraseña o clave de recuperación", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Por favor, envíe un email a <a>{{emailID}}</a> desde su dirección de correo electrónico registrada", + "CONTACT_SUPPORT": "Contacta con soporte", + "REQUEST_FEATURE": "Solicitar una función", + "SUPPORT": "Soporte", + "CONFIRM": "Confirmar", + "CANCEL": "Cancelar", + "LOGOUT": "Cerrar sesión", + "DELETE_ACCOUNT": "Eliminar cuenta", + "DELETE_ACCOUNT_MESSAGE": "<p>Por favor, envíe un email a <a>{{emailID}}</a> desde su dirección de correo electrónico registrada</p><p>Su solicitud será procesada en 72 horas.</p>", + "LOGOUT_MESSAGE": "Seguro que quiere cerrar la sesión?", + "CHANGE_EMAIL": "Cambiar email", + "OK": "OK", + "SUCCESS": "Completado", + "ERROR": "Error", + "MESSAGE": "Mensaje", + "INSTALL_MOBILE_APP": "Instala nuestra aplicación <a>Android</a> o <b>iOS</b> para hacer una copia de seguridad automática de todas usted fotos", + "DOWNLOAD_APP_MESSAGE": "Lo sentimos, esta operación sólo es compatible con nuestra aplicación de computadora", + "DOWNLOAD_APP": "Descargar aplicación de computadora", + "EXPORT": "Exportar datos", + "SUBSCRIPTION": "Suscripción", + "SUBSCRIBE": "Suscribir", + "MANAGEMENT_PORTAL": "Gestionar métodos de pago", + "MANAGE_FAMILY_PORTAL": "Administrar familia", + "LEAVE_FAMILY_PLAN": "Dejar plan familiar", + "LEAVE": "Dejar", + "LEAVE_FAMILY_CONFIRM": "Está seguro de que desea abandonar el plan familiar?", + "CHOOSE_PLAN": "Elije tu plan", + "MANAGE_PLAN": "Administra tu suscripción", + "ACTIVE": "Activo", + "OFFLINE_MSG": "Estás desconectado, se están mostrando recuerdos en caché", + "FREE_SUBSCRIPTION_INFO": "Estás en el plan <strong>gratis</strong> que expira el {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Estás en un plan familiar administrado por", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Se renueva en {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Termina el {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Tu suscripción será cancelada el {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Ha excedido su cuota de almacenamiento, por favor <a>actualice</a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>Hemos recibido tu pago</p><p>¡Tu suscripción es válida hasta <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Tu compra ha sido cancelada, por favor inténtalo de nuevo si quieres suscribirte", + "SUBSCRIPTION_PURCHASE_FAILED": "Compra de suscripción fallida, por favor inténtalo de nuevo", + "SUBSCRIPTION_UPDATE_FAILED": "Suscripción actualizada falló, inténtelo de nuevo", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Lo sentimos, el pago falló cuando intentamos cargar a su tarjeta, por favor actualice su método de pago y vuelva a intentarlo", + "STRIPE_AUTHENTICATION_FAILED": "No podemos autenticar tu método de pago. Por favor, elige un método de pago diferente e inténtalo de nuevo", + "UPDATE_PAYMENT_METHOD": "Actualizar medio de pago", + "MONTHLY": "Mensual", + "YEARLY": "Anual", + "UPDATE_SUBSCRIPTION_MESSAGE": "Seguro de que desea cambiar su plan?", + "UPDATE_SUBSCRIPTION": "Cambiar de plan", + "CANCEL_SUBSCRIPTION": "Cancelar suscripción", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Todos tus datos serán eliminados de nuestros servidores al final de este periodo de facturación.</p><p>¿Está seguro de que desea cancelar su suscripción?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "No se pudo cancelar la suscripción", + "SUBSCRIPTION_CANCEL_SUCCESS": "Suscripción cancelada correctamente", + "REACTIVATE_SUBSCRIPTION": "Reactivar la suscripción", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Una vez reactivado, serás facturado el {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Suscripción activada correctamente ", + "SUBSCRIPTION_ACTIVATE_FAILED": "No se pudo reactivar las renovaciones de suscripción", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Gracias", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancelar suscripción a móviles", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Por favor, cancele su suscripción de la aplicación móvil para activar una suscripción aquí", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Por favor, contáctenos en <a>{{emailID}}</a> para gestionar su suscripción", + "RENAME": "Renombrar", + "RENAME_FILE": "Renombrar archivo", + "RENAME_COLLECTION": "Renombrar álbum", + "DELETE_COLLECTION_TITLE": "Eliminar álbum?", + "DELETE_COLLECTION": "Eliminar álbum", + "DELETE_COLLECTION_MESSAGE": "También eliminar las fotos (y los vídeos) presentes en este álbum de <a>todos</a> álbumes de los que forman parte?", + "DELETE_PHOTOS": "Eliminar fotos", + "KEEP_PHOTOS": "Conservar fotos", + "SHARE": "Compartir", + "SHARE_COLLECTION": "Compartir álbum", + "SHAREES": "Compartido con", + "SHARE_WITH_SELF": "Uy, no puedes compartir contigo mismo", + "ALREADY_SHARED": "Uy, ya estás compartiendo esto con {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Compartir álbum no permitido", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Compartir está desactivado para cuentas gratis", + "DOWNLOAD_COLLECTION": "Descargar álbum", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>¿Está seguro de que desea descargar el álbum completo?</p><p>Todos los archivos se pondrán en cola para su descarga secuencialmente</p>", + "CREATE_ALBUM_FAILED": "Error al crear el álbum, inténtalo de nuevo", + "SEARCH": "Buscar", + "SEARCH_RESULTS": "Buscar resultados", + "NO_RESULTS": "No se han encontrado resultados", + "SEARCH_HINT": "Buscar álbumes, fechas...", + "SEARCH_TYPE": { + "COLLECTION": "Álbum", + "LOCATION": "Localización", + "CITY": "", + "DATE": "Fecha", + "FILE_NAME": "Nombre del archivo", + "THING": "Contenido", + "FILE_CAPTION": "Descripción", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "No hay recuerdos", + "photos_count_one": "1 recuerdo", + "photos_count_other": "{{count}} recuerdos", + "TERMS_AND_CONDITIONS": "Acepto los <a>términos</a> y <b>política de privacidad</b>", + "ADD_TO_COLLECTION": "Añadir al álbum", + "SELECTED": "seleccionado", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Este vídeo no se puede reproducir en tu navegador", + "PEOPLE": "Personajes", + "INDEXING_SCHEDULED": "el indexado está programado...", + "ANALYZING_PHOTOS": "analizando nuevas fotos {{indexStatus.nSyncedFiles}} de {{indexStatus.nTotalFiles}} hecho)...", + "INDEXING_PEOPLE": "indexando personas en {{indexStatus.nSyncedFiles}} fotos... ", + "INDEXING_DONE": "fotos {{indexStatus.nSyncedFiles}} indexadas", + "UNIDENTIFIED_FACES": "caras no identificadas", + "OBJECTS": "objetos", + "TEXT": "texto", + "INFO": "Info ", + "INFO_OPTION": "Info (I)", + "FILE_NAME": "Nombre del archivo", + "CAPTION_PLACEHOLDER": "Añadir una descripción", + "LOCATION": "Localización", + "SHOW_ON_MAP": "Ver en OpenStreetMap", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "Detalles", + "VIEW_EXIF": "Ver todos los datos de EXIF", + "NO_EXIF": "No hay datos EXIF", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Dos factores", + "TWO_FACTOR_AUTHENTICATION": "Autenticación de dos factores", + "TWO_FACTOR_QR_INSTRUCTION": "Escanea el código QR de abajo con tu aplicación de autenticación favorita", + "ENTER_CODE_MANUALLY": "Ingrese el código manualmente", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Por favor, introduce este código en tu aplicación de autenticación favorita", + "SCAN_QR_CODE": "Escanear código QR en su lugar", + "ENABLE_TWO_FACTOR": "Activar dos factores", + "ENABLE": "Activar", + "LOST_DEVICE": "Perdido el dispositivo de doble factor", + "INCORRECT_CODE": "Código incorrecto", + "TWO_FACTOR_INFO": "Añade una capa adicional de seguridad al requerir más de tu email y contraseña para iniciar sesión en tu cuenta", + "DISABLE_TWO_FACTOR_LABEL": "Deshabilitar la autenticación de dos factores", + "UPDATE_TWO_FACTOR_LABEL": "Actualice su dispositivo de autenticación", + "DISABLE": "Desactivar", + "RECONFIGURE": "Reconfigurar", + "UPDATE_TWO_FACTOR": "Actualizar doble factor", + "UPDATE_TWO_FACTOR_MESSAGE": "Continuar adelante anulará los autenticadores previamente configurados", + "UPDATE": "Actualizar", + "DISABLE_TWO_FACTOR": "Desactivar doble factor", + "DISABLE_TWO_FACTOR_MESSAGE": "¿Estás seguro de que desea deshabilitar la autenticación de doble factor?", + "TWO_FACTOR_DISABLE_FAILED": "Error al desactivar dos factores, inténtalo de nuevo", + "EXPORT_DATA": "Exportar datos", + "SELECT_FOLDER": "Seleccionar carpeta", + "DESTINATION": "Destinación", + "START": "Inicio", + "LAST_EXPORT_TIME": "Fecha de la última exportación", + "EXPORT_AGAIN": "Resinc", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Almacenamiento local inaccesible", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Su navegador o un addon está bloqueando a ente de guardar datos en almacenamiento local. Por favor, intente cargar esta página después de cambiar su modo de navegación.", + "SEND_OTT": "Enviar OTP", + "EMAIl_ALREADY_OWNED": "Email ya tomado", + "ETAGS_BLOCKED": "<p>No hemos podido subir los siguientes archivos debido a la configuración de tu navegador.</p><p>Por favor, deshabilite cualquier complemento que pueda estar impidiendo que ente utilice <code>eTags</code> para subir archivos grandes, o utilice nuestra <a>aplicación de escritorio</a> para una experiencia de importación más fiable.</p>", + "SKIPPED_VIDEOS_INFO": "<p>Actualmente no podemos añadir vídeos a través de enlaces públicos.</p><p>Para compartir vídeos, por favor <a>regístrate</a> en ente y comparte con los destinatarios a través de su correo electrónico.</p>", + "LIVE_PHOTOS_DETECTED": "Los archivos de foto y vídeo de tus fotos en vivo se han fusionado en un solo archivo", + "RETRY_FAILED": "Reintentar subidas fallidas", + "FAILED_UPLOADS": "Subidas fallidas ", + "SKIPPED_FILES": "Subidas ignoradas", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Generación de miniaturas fallida", + "UNSUPPORTED_FILES": "Archivos no soportados", + "SUCCESSFUL_UPLOADS": "Subidas exitosas", + "SKIPPED_INFO": "Se han omitido ya que hay archivos con nombres coincidentes en el mismo álbum", + "UNSUPPORTED_INFO": "ente no soporta estos formatos de archivo aún", + "BLOCKED_UPLOADS": "Subidas bloqueadas", + "SKIPPED_VIDEOS": "Vídeos saltados", + "INPROGRESS_METADATA_EXTRACTION": "En proceso", + "INPROGRESS_UPLOADS": "Subidas en progreso", + "TOO_LARGE_UPLOADS": "Archivos grandes", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Espacio insuficiente", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Estos archivos no se han subido porque exceden el límite de tamaño máximo para tu plan de almacenamiento", + "TOO_LARGE_INFO": "Estos archivos no se han subido porque exceden nuestro límite máximo de tamaño de archivo", + "THUMBNAIL_GENERATION_FAILED_INFO": "Estos archivos fueron cargados, pero por desgracia no pudimos generar las miniaturas para ellos.", + "UPLOAD_TO_COLLECTION": "Subir al álbum", + "UNCATEGORIZED": "No clasificado", + "ARCHIVE": "Archivo", + "FAVORITES": "Favoritos", + "ARCHIVE_COLLECTION": "Archivo álbum", + "ARCHIVE_SECTION_NAME": "Archivo", + "ALL_SECTION_NAME": "Todo", + "MOVE_TO_COLLECTION": "Mover al álbum", + "UNARCHIVE": "Desarchivar", + "UNARCHIVE_COLLECTION": "Desarchivar álbum", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "Mover", + "ADD": "Añadir", + "REMOVE": "Eliminar", + "YES_REMOVE": "Sí, eliminar", + "REMOVE_FROM_COLLECTION": "Eliminar del álbum", + "TRASH": "Papelera", + "MOVE_TO_TRASH": "Mover a la papelera", + "TRASH_FILES_MESSAGE": "Los archivos seleccionados serán eliminados de todos los álbumes y movidos a la papelera.", + "TRASH_FILE_MESSAGE": "El archivo será eliminado de todos los álbumes y movido a la papelera.", + "DELETE_PERMANENTLY": "Eliminar para siempre", + "RESTORE": "Restaurar", + "RESTORE_TO_COLLECTION": "Restaurar al álbum", + "EMPTY_TRASH": "Vaciar papelera", + "EMPTY_TRASH_TITLE": "Vaciar papelera?", + "EMPTY_TRASH_MESSAGE": "Estos archivos serán eliminados permanentemente de su cuenta ente.", + "LEAVE_SHARED_ALBUM": "Sí, dejar", + "LEAVE_ALBUM": "Dejar álbum", + "LEAVE_SHARED_ALBUM_TITLE": "¿Dejar álbum compartido?", + "LEAVE_SHARED_ALBUM_MESSAGE": "Dejará el álbum, y dejará de ser visible para usted.", + "NOT_FILE_OWNER": "No puedes eliminar archivos de un álbum compartido", + "CONFIRM_SELF_REMOVE_MESSAGE": "Los elementos seleccionados serán eliminados de este álbum. Los elementos que estén sólo en este álbum serán movidos a Sin categorizar.", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Algunos de los elementos que estás eliminando fueron añadidos por otras personas, y perderás el acceso a ellos.", + "SORT_BY_CREATION_TIME_ASCENDING": "Antiguo", + "SORT_BY_UPDATION_TIME_DESCENDING": "Última actualización", + "SORT_BY_NAME": "Nombre", + "COMPRESS_THUMBNAILS": "Comprimir las miniaturas", + "THUMBNAIL_REPLACED": "Miniaturas comprimidas", + "FIX_THUMBNAIL": "Comprimir", + "FIX_THUMBNAIL_LATER": "Comprimir más tarde", + "REPLACE_THUMBNAIL_NOT_STARTED": "Algunas de tus miniaturas de vídeos pueden ser comprimidas para ahorrar espacio. ¿Te gustaría que ente las comprima?", + "REPLACE_THUMBNAIL_COMPLETED": "Todas las miniaturas se comprimieron con éxito", + "REPLACE_THUMBNAIL_NOOP": "No tienes miniaturas que se puedan comprimir más", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "No se pudieron comprimir algunas de tus miniaturas, por favor inténtalo de nuevo", + "FIX_CREATION_TIME": "Fijar hora", + "FIX_CREATION_TIME_IN_PROGRESS": "Fijar hora", + "CREATION_TIME_UPDATED": "Hora del archivo actualizada", + "UPDATE_CREATION_TIME_NOT_STARTED": "Seleccione la cartera que desea utilizar", + "UPDATE_CREATION_TIME_COMPLETED": "Todos los archivos se han actualizado correctamente", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Fallo en la hora del archivo para algunos archivos, por favor inténtelo de nuevo", + "CAPTION_CHARACTER_LIMIT": "Máximo 5000 caracteres", + "DATE_TIME_ORIGINAL": "EXIF: Fecha original", + "DATE_TIME_DIGITIZED": "EXIF: Fecha Digitalizado", + "METADATA_DATE": "", + "CUSTOM_TIME": "Hora personalizada", + "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planes", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Error al abrir los planes", + "INSTALL": "Instalar", + "SHARING_DETAILS": "Compartir detalles", + "MODIFY_SHARING": "Modificar compartir", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "Propietario", + "COLLABORATORS": "Colaboradores", + "ADD_MORE": "Añadir más", + "VIEWERS": "", + "OR_ADD_EXISTING": "O elige uno existente", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "404 - No Encontrado", + "LINK_EXPIRED": "Enlace expirado", + "LINK_EXPIRED_MESSAGE": "Este enlace ha caducado o ha sido desactivado!", + "MANAGE_LINK": "Administrar enlace", + "LINK_TOO_MANY_REQUESTS": "Este álbum es demasiado popular para que podamos manejarlo!", + "FILE_DOWNLOAD": "Permitir descargas", + "LINK_PASSWORD_LOCK": "Contraseña bloqueada", + "PUBLIC_COLLECT": "Permitir añadir fotos", + "LINK_DEVICE_LIMIT": "Límites del dispositivo", + "NO_DEVICE_LIMIT": "Ninguno", + "LINK_EXPIRY": "Enlace vencio", + "NEVER": "Nunca", + "DISABLE_FILE_DOWNLOAD": "Deshabilitar descarga", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>¿Está seguro que desea desactivar el botón de descarga de archivos?</p><p>Los visualizadores todavía pueden tomar capturas de pantalla o guardar una copia de sus fotos usando herramientas externas.</p>", + "MALICIOUS_CONTENT": "Contiene contenido malicioso", + "COPYRIGHT": "Infracciones sobre los derechos de autor de alguien que estoy autorizado a representar", + "SHARED_USING": "Compartido usando ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "Usa el código <strong>{{referralCode}}</strong> para obtener 10 GB gratis", + "LIVE": "VIVO", + "DISABLE_PASSWORD": "Desactivar contraseña", + "DISABLE_PASSWORD_MESSAGE": "Seguro que quieres cambiar la contrasena?", + "PASSWORD_LOCK": "Contraseña bloqueada", + "LOCK": "Bloquear", + "DOWNLOAD_UPLOAD_LOGS": "Logs de depuración", + "UPLOAD_FILES": "Archivo", + "UPLOAD_DIRS": "Carpeta", + "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", + "DEDUPLICATE_FILES": "Deduplicar archivos", + "AUTHENTICATOR_SECTION": "Autenticación", + "NO_DUPLICATES_FOUND": "No tienes archivos duplicados que puedan ser borrados", + "CLUB_BY_CAPTURE_TIME": "Club por tiempo de captura", + "FILES": "Archivos", + "EACH": "Cada", + "DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados", + "STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?", + "STOP_UPLOADS_HEADER": "Detener las subidas?", + "YES_STOP_UPLOADS": "Sí, detener las subidas", + "STOP_DOWNLOADS_HEADER": "¿Detener las descargas?", + "YES_STOP_DOWNLOADS": "Sí, detener las descargas", + "STOP_ALL_DOWNLOADS_MESSAGE": "¿Estás seguro de que quieres detener todas las descargas en curso?", + "albums_one": "1 álbum", + "albums_other": "{{count}} álbumes", + "ALL_ALBUMS": "Todos los álbumes", + "ALBUMS": "Álbumes", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "Ingrese el código de seis dígitos de su aplicación de autenticación a continuación.", + "CREATE_ACCOUNT": "Crear cuenta", + "COPIED": "Copiado", + "CANVAS_BLOCKED_TITLE": "No se puede generar la miniatura", + "CANVAS_BLOCKED_MESSAGE": "<p>Parece que su navegador ha deshabilitado el acceso al lienzo, que es necesario para generar miniaturas para tus fotos </p> <p> Por favor, activa el acceso al lienzo de tu navegador, o revisa nuestra aplicación de escritorio</p>", + "WATCH_FOLDERS": "Ver carpetas", + "UPGRADE_NOW": "Mejorar ahora", + "RENEW_NOW": "Renovar ahora", + "STORAGE": "Almacén", + "USED": "usado", + "YOU": "Usted", + "FAMILY": "Familia", + "FREE": "gratis", + "OF": "de", + "WATCHED_FOLDERS": "Ver carpetas", + "NO_FOLDERS_ADDED": "No hay carpetas añadidas!", + "FOLDERS_AUTOMATICALLY_MONITORED": "Las carpetas que añadas aquí serán supervisadas automáticamente", + "UPLOAD_NEW_FILES_TO_ENTE": "Subir nuevos archivos a ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Eliminar archivos borrados de ente", + "ADD_FOLDER": "Añadir carpeta", + "STOP_WATCHING": "Dejar de ver", + "STOP_WATCHING_FOLDER": "Dejar de ver carpeta?", + "STOP_WATCHING_DIALOG_MESSAGE": "Tus archivos existentes no serán eliminados, pero ente dejará de actualizar automáticamente el álbum enlazado en caso de cambios en esta carpeta.", + "YES_STOP": "Sí, detener", + "MONTH_SHORT": "mes", + "YEAR": "año", + "FAMILY_PLAN": "Plan familiar", + "DOWNLOAD_LOGS": "Descargar logs", + "DOWNLOAD_LOGS_MESSAGE": "<p>Esto descargará los registros de depuración, que puede enviarnos por correo electrónico para ayudarnos a depurar su problema.</p><p> Tenga en cuenta que los nombres de los archivos se incluirán para ayudar al seguimiento de problemas con archivos específicos. </p>", + "CHANGE_FOLDER": "Cambiar carpeta", + "TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales", + "GB": "GB", + "POPULAR": "Popular", + "FREE_PLAN_OPTION_LABEL": "Continuar con el plan gratuito", + "FREE_PLAN_DESCRIPTION": "1 GB por 1 año", + "CURRENT_USAGE": "El uso actual es <strong>{{usage}}</strong>", + "WEAK_DEVICE": "El navegador web que está utilizando no es lo suficientemente poderoso para cifrar sus fotos. Por favor, intente iniciar sesión en ente en su computadora, o descargue la aplicación ente para móvil/escritorio.", + "DRAG_AND_DROP_HINT": "O arrastre y suelte en la ventana ente", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Los datos subidos se eliminarán y su cuenta se eliminará de forma permanente.<br/><br/>Esta acción no es reversible.", + "AUTHENTICATE": "Autenticado", + "UPLOADED_TO_SINGLE_COLLECTION": "Subir a una sola colección", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "Subir a colecciones separadas", + "NEVERMIND": "No importa", + "UPDATE_AVAILABLE": "Actualizacion disponible", + "UPDATE_INSTALLABLE_MESSAGE": "Una nueva versión de ente está lista para ser instalada.", + "INSTALL_NOW": "Instalar ahora", + "INSTALL_ON_NEXT_LAUNCH": "Instalar en el próximo lanzamiento", + "UPDATE_AVAILABLE_MESSAGE": "Una nueva versión de ente ha sido lanzada, pero no se puede descargar e instalar automáticamente.", + "DOWNLOAD_AND_INSTALL": "Descargar e instalar", + "IGNORE_THIS_VERSION": "Ignorar esta versión", + "TODAY": "Hoy", + "YESTERDAY": "Ayer", + "NAME_PLACEHOLDER": "Nombre...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "No se puede crear álbumes de mezcla de archivos/carpetas", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Has arrastrado y soltado una mezcla de archivos y carpetas.</p><p>Por favor proporcione sólo archivos o carpetas cuando seleccione la opción de crear álbumes separados</p>", + "CHOSE_THEME": "Elegir tema", + "ML_SEARCH": "Buscar ML (beta)", + "ENABLE_ML_SEARCH_DESCRIPTION": "<p>Esto permitirá el aprendizaje automático en el dispositivo y la búsqueda facial que comenzará a analizar las fotos subidas localmente.</p><p>Para la primera ejecución después de iniciar sesión o habilitar esta función, se descargarán todas las imágenes en el dispositivo local para analizarlas. Así que por favor actívalo sólo si dispones ancho de banda y el almacenamiento suficiente para el procesamiento local de todas las imágenes en tu biblioteca de fotos.</p><p>Si esta es la primera vez que está habilitando, también le pediremos su permiso para procesar los datos faciales.</p>", + "ML_MORE_DETAILS": "Más detalles", + "ENABLE_FACE_SEARCH": "Activar búsqueda facial", + "ENABLE_FACE_SEARCH_TITLE": "Activar búsqueda facial?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "<p>Si activas la búsqueda facial, ente extraerá la geometría facial de tus fotos. Esto sucederá en su dispositivo y cualquier dato biométrico generado será cifrado de extremo a extremo.<p/><p><a>Haga clic aquí para obtener más detalles sobre esta característica en nuestra política de privacidad</a></p>", + "DISABLE_BETA": "Desactivar beta", + "DISABLE_FACE_SEARCH": "Desactivar búsqueda facial", + "DISABLE_FACE_SEARCH_TITLE": "Desactivar búsqueda facial?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente dejará de procesar la geometría facial, y también desactivará la búsqueda ML (beta)</p><p>Puede volver a activar la búsqueda facial si lo desea, ya que esta operación es segura.</p>", + "ADVANCED": "Avanzado", + "FACE_SEARCH_CONFIRMATION": "Comprendo y deseo permitir que ente procese la geometría de la cara", + "LABS": "Labs", + "YOURS": "tuyo", + "PASSPHRASE_STRENGTH_WEAK": "Fortaleza de la contraseña: débil", + "PASSPHRASE_STRENGTH_MODERATE": "Fortaleza de contraseña: Moderar", + "PASSPHRASE_STRENGTH_STRONG": "Fortaleza de contraseña: fuerte", + "PREFERENCES": "Preferencias", + "LANGUAGE": "Idioma", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Archivo de exportación inválido", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>El directorio de exportación seleccionado no existe.</p><p> Por favor, seleccione un directorio válido.</p>", + "SUBSCRIPTION_VERIFICATION_ERROR": "Falló la verificación de la suscripción", + "STORAGE_UNITS": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "después de una hora", + "DAY": "después de un día", + "WEEK": "después de una semana", + "MONTH": "después de un mes", + "YEAR": "después de un año" + }, + "COPY_LINK": "Copiar enlace", + "DONE": "Hecho", + "LINK_SHARE_TITLE": "O comparte un enlace", + "REMOVE_LINK": "Eliminar enlace", + "CREATE_PUBLIC_SHARING": "Crear un enlace público", + "PUBLIC_LINK_CREATED": "Enlace público creado", + "PUBLIC_LINK_ENABLED": "Enlace público activado", + "COLLECT_PHOTOS": "Obtener fotos", + "PUBLIC_COLLECT_SUBTEXT": "Permitir a las personas con el enlace añadir fotos al álbum compartido.", + "STOP_EXPORT": "Stop", + "EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> archivos exportados", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "Exportar iniciando", + "IN_PROGRESS": "Exportación ya en curso", + "FINISH": "Exportación finalizada", + "UP_TO_DATE": "No hay nuevos archivos para exportar" + }, + "CONTINUOUS_EXPORT": "Sincronizar continuamente", + "TOTAL_ITEMS": "Total de elementos", + "PENDING_ITEMS": "Elementos pendientes", + "EXPORT_STARTING": "Exportar iniciando...", + "DELETE_ACCOUNT_REASON_LABEL": "¿Cuál es la razón principal por la que eliminas tu cuenta?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Selecciona una razón", + "DELETE_REASON": { + "MISSING_FEATURE": "Falta una característica clave que necesito", + "BROKEN_BEHAVIOR": "La aplicación o una característica determinada no se comporta como creo que debería", + "FOUND_ANOTHER_SERVICE": "He encontrado otro servicio que me gusta más", + "NOT_LISTED": "Mi motivo no se encuentra en la lista" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "Lamentamos que te vayas. Explica por qué te vas para ayudarnos a mejorar.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Sugerencias", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Sí, quiero eliminar permanentemente esta cuenta y todos sus datos", + "CONFIRM_DELETE_ACCOUNT": "Corfirmar borrado de cuenta", + "FEEDBACK_REQUIRED": "Ayúdanos con esta información", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Qué hace mejor el otro servicio?", + "RECOVER_TWO_FACTOR": "Recuperar dos factores", + "at": "a las", + "AUTH_NEXT": "siguiente", + "AUTH_DOWNLOAD_MOBILE_APP": "Descarga nuestra aplicación móvil para administrar tus secretos", + "HIDDEN": "", + "HIDE": "Ocultar", + "UNHIDE": "Mostrar", + "UNHIDE_TO_COLLECTION": "Hacer visible al álbum", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "Video", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "Transformar", + "COLORS": "Colores", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/fa-IR/translation.json b/web/apps/cast/public/locales/fa-IR/translation.json new file mode 100644 index 000000000..2d21fcb3d --- /dev/null +++ b/web/apps/cast/public/locales/fa-IR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "به <a/> خوش آمدید", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/fi-FI/translation.json b/web/apps/cast/public/locales/fi-FI/translation.json new file mode 100644 index 000000000..888ed7093 --- /dev/null +++ b/web/apps/cast/public/locales/fi-FI/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/fr-FR/translation.json b/web/apps/cast/public/locales/fr-FR/translation.json new file mode 100644 index 000000000..43d959069 --- /dev/null +++ b/web/apps/cast/public/locales/fr-FR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Sauvegardes privées</div><div>pour vos souvenirs</div>", + "HERO_SLIDE_1": "Chiffrement de bout en bout par défaut", + "HERO_SLIDE_2_TITLE": "<div>Sécurisé </div><div>dans un abri antiatomique</div>", + "HERO_SLIDE_2": "Conçu pour survivre", + "HERO_SLIDE_3_TITLE": "<div>Disponible</div><div> en tout lieu</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Ordinateur", + "LOGIN": "Connexion", + "SIGN_UP": "Inscription", + "NEW_USER": "Nouveau sur ente", + "EXISTING_USER": "Utilisateur existant", + "ENTER_NAME": "Saisir un nom", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Ajouter un nom afin que vos amis sachent qui remercier pour ces magnifiques photos!", + "ENTER_EMAIL": "Saisir l'adresse e-mail", + "EMAIL_ERROR": "Saisir un e-mail valide", + "REQUIRED": "Nécessaire", + "EMAIL_SENT": "Code de vérification envoyé à <a>{{email}}</a>", + "CHECK_INBOX": "Veuillez consulter votre boite de réception (et indésirables) pour poursuivre la vérification", + "ENTER_OTT": "Code de vérification", + "RESEND_MAIL": "Renvoyer le code", + "VERIFY": "Vérifier", + "UNKNOWN_ERROR": "Quelque chose s'est mal passé, veuillez recommencer", + "INVALID_CODE": "Code de vérification non valide", + "EXPIRED_CODE": "Votre code de vérification a expiré", + "SENDING": "Envoi...", + "SENT": "Envoyé!", + "PASSWORD": "Mot de passe", + "LINK_PASSWORD": "Saisir le mot de passe pour déverrouiller l'album", + "RETURN_PASSPHRASE_HINT": "Mot de passe", + "SET_PASSPHRASE": "Définir le mot de passe", + "VERIFY_PASSPHRASE": "Connexion", + "INCORRECT_PASSPHRASE": "Mot de passe non valide", + "ENTER_ENC_PASSPHRASE": "Veuillez saisir un mot de passe que nous pourrons utiliser pour chiffrer vos données", + "PASSPHRASE_DISCLAIMER": "Nous ne stockons pas votre mot de passe, donc si vous le perdez, <strong>nous ne pourrons pas vous aider</strong> à récupérer vos données sans une clé de récupération.", + "WELCOME_TO_ENTE_HEADING": "Bienvenue sur <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Stockage et partage photo avec cryptage de bout en bout", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Là où vivent vos meilleures photos", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Génération des clés de chiffrement...", + "PASSPHRASE_HINT": "Mot de passe", + "CONFIRM_PASSPHRASE": "Confirmer le mot de passe", + "REFERRAL_CODE_HINT": "Comment avez-vous entendu parler de Ente? (facultatif)", + "REFERRAL_INFO": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", + "PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas", + "CREATE_COLLECTION": "Nouvel album", + "ENTER_ALBUM_NAME": "Nom de l'album", + "CLOSE_OPTION": "Fermer (Échap)", + "ENTER_FILE_NAME": "Nom du fichier", + "CLOSE": "Fermer", + "NO": "Non", + "NOTHING_HERE": "Il n'y a encore rien à voir ici 👀", + "UPLOAD": "Charger", + "IMPORT": "Importer", + "ADD_PHOTOS": "Ajouter des photos", + "ADD_MORE_PHOTOS": "Ajouter plus de photos", + "add_photos_one": "Ajouter une photo", + "add_photos_other": "Ajouter {{count}} photos", + "SELECT_PHOTOS": "Sélectionner des photos", + "FILE_UPLOAD": "Fichier chargé", + "UPLOAD_STAGE_MESSAGE": { + "0": "Préparation du chargement", + "1": "Lecture des fichiers de métadonnées de Google", + "2": "Métadonnées des fichiers {{uploadCounter.finished}} / {{uploadCounter.total}} extraites", + "3": "{{uploadCounter.finished}} / {{uploadCounter.total}} fichiers sauvegardés", + "4": "Annulation des chargements restants", + "5": "Sauvegarde terminée" + }, + "FILE_NOT_UPLOADED_LIST": "Les fichiers suivants n'ont pas été chargés", + "SUBSCRIPTION_EXPIRED": "Abonnement expiré", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Votre abonnement a expiré, veuillez <a>le renouveler </a>", + "STORAGE_QUOTA_EXCEEDED": "Limite de stockage atteinte", + "INITIAL_LOAD_DELAY_WARNING": "La première consultation peut prendre du temps", + "USER_DOES_NOT_EXIST": "Désolé, impossible de trouver un utilisateur avec cet e-mail", + "NO_ACCOUNT": "Je n'ai pas de compte", + "ACCOUNT_EXISTS": "J'ai déjà un compte", + "CREATE": "Créer", + "DOWNLOAD": "Télécharger", + "DOWNLOAD_OPTION": "Télécharger (D)", + "DOWNLOAD_FAVORITES": "Télécharger les favoris", + "DOWNLOAD_UNCATEGORIZED": "Télécharger les hors catégories", + "DOWNLOAD_HIDDEN_ITEMS": "Télécharger les fichiers masqués", + "COPY_OPTION": "Copier en PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Plein écran (F)", + "ZOOM_IN_OUT": "Zoom +/-", + "PREVIOUS": "Précédent (←)", + "NEXT": "Suivant (→)", + "TITLE_PHOTOS": "Ente Photos", + "TITLE_ALBUMS": "Ente Photos", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "Chargez votre 1ere photo", + "IMPORT_YOUR_FOLDERS": "Importez vos dossiers", + "UPLOAD_DROPZONE_MESSAGE": "Déposez pour sauvegarder vos fichiers", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Déposez pour ajouter un dossier surveillé", + "TRASH_FILES_TITLE": "Supprimer les fichiers ?", + "TRASH_FILE_TITLE": "Supprimer le fichier ?", + "DELETE_FILES_TITLE": "Supprimer immédiatement?", + "DELETE_FILES_MESSAGE": "Les fichiers sélectionnés seront définitivement supprimés de votre compte ente.", + "DELETE": "Supprimer", + "DELETE_OPTION": "Supprimer (DEL)", + "FAVORITE_OPTION": "Favori (L)", + "UNFAVORITE_OPTION": "Non favori (L)", + "MULTI_FOLDER_UPLOAD": "Plusieurs dossiers détectés", + "UPLOAD_STRATEGY_CHOICE": "Voulez-vous les charger dans", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Un seul album", + "OR": "ou", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Albums séparés", + "SESSION_EXPIRED_MESSAGE": "Votre session a expiré, veuillez vous reconnecter pour poursuivre", + "SESSION_EXPIRED": "Session expiré", + "PASSWORD_GENERATION_FAILED": "Votre navigateur ne permet pas de générer une clé forte correspondant aux standards de chiffrement de ente, veuillez réessayer en utilisant l'appli mobile ou un autre navigateur", + "CHANGE_PASSWORD": "Modifier le mot de passe", + "GO_BACK": "Retour", + "RECOVERY_KEY": "Clé de récupération", + "SAVE_LATER": "Plus tard", + "SAVE": "Sauvegarder la clé", + "RECOVERY_KEY_DESCRIPTION": "Si vous oubliez votre mot de passe, la seule façon de récupérer vos données sera grâce à cette clé.", + "RECOVER_KEY_GENERATION_FAILED": "Le code de récupération ne peut être généré, veuillez réessayer", + "KEY_NOT_STORED_DISCLAIMER": "Nous ne stockons pas cette clé, veuillez donc la sauvegarder dans un endroit sûr", + "FORGOT_PASSWORD": "Mot de passe oublié", + "RECOVER_ACCOUNT": "Récupérer le compte", + "RECOVERY_KEY_HINT": "Clé de récupération", + "RECOVER": "Récupérer", + "NO_RECOVERY_KEY": "Pas de clé de récupération?", + "INCORRECT_RECOVERY_KEY": "Clé de récupération non valide", + "SORRY": "Désolé", + "NO_RECOVERY_KEY_MESSAGE": "En raison de notre protocole de chiffrement de bout en bout, vos données ne peuvent être décryptées sans votre mot de passe ou clé de récupération", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Veuillez envoyer un e-mail à <a>{{emailID}}</a> depuis votre adresse enregistrée", + "CONTACT_SUPPORT": "Contacter le support", + "REQUEST_FEATURE": "Soumettre une idée", + "SUPPORT": "Support", + "CONFIRM": "Confirmer", + "CANCEL": "Annuler", + "LOGOUT": "Déconnexion", + "DELETE_ACCOUNT": "Supprimer le compte", + "DELETE_ACCOUNT_MESSAGE": "<p>Veuillez envoyer un e-mail à <a>{{emailID}}</a>depuis Votre adresse enregistrée.</p><p> Votre demande sera traitée dans les 72 heures.</p>", + "LOGOUT_MESSAGE": "Voulez-vous vraiment vous déconnecter?", + "CHANGE_EMAIL": "Modifier l'e-mail", + "OK": "Ok", + "SUCCESS": "Parfait", + "ERROR": "Erreur", + "MESSAGE": "Message", + "INSTALL_MOBILE_APP": "Installez notre application <a>Android</a> or <b>iOS</b> pour sauvegarder automatiquement toutes vos photos", + "DOWNLOAD_APP_MESSAGE": "Désolé, cette opération est actuellement supportée uniquement sur notre appli pour ordinateur", + "DOWNLOAD_APP": "Télécharger l'appli pour ordinateur", + "EXPORT": "Exporter des données", + "SUBSCRIPTION": "Abonnement", + "SUBSCRIBE": "S'abonner", + "MANAGEMENT_PORTAL": "Gérer le mode de paiement", + "MANAGE_FAMILY_PORTAL": "Gérer la famille", + "LEAVE_FAMILY_PLAN": "Quitter le plan famille", + "LEAVE": "Quitter", + "LEAVE_FAMILY_CONFIRM": "Êtes-vous certains de vouloir quitter le plan famille?", + "CHOOSE_PLAN": "Choisir votre plan", + "MANAGE_PLAN": "Gérer votre abonnement", + "ACTIVE": "Actif", + "OFFLINE_MSG": "Vous êtes hors-ligne, les mémoires cache sont affichées", + "FREE_SUBSCRIPTION_INFO": "Vous êtes sur le plan <strong>gratuit</strong> qui expire le {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Vous êtes sur le plan famille géré par", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renouveler le {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Pris fin le {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Votre abonnement sera annulé le {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Votre module {{storage, string}} est valable jusqu'au {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Vous avez dépassé votre quota de stockage, veuillez <a> mettre à niveau </a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>Nous avons reçu votre paiement </p><p>Votre abonnement est valide jusqu'au <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Votre achat est annulé, veuillez réessayer si vous souhaitez vous abonner", + "SUBSCRIPTION_PURCHASE_FAILED": "Échec lors de l'achat de l'abonnement, veuillez réessayer", + "SUBSCRIPTION_UPDATE_FAILED": "Échec lors de la mise à niveau de l'abonnement, veuillez réessayer", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Désolé, échec de paiement lors de la saisie de votre carte, veuillez mettr eà jour votre moyen de paiement et réessayer", + "STRIPE_AUTHENTICATION_FAILED": "Nous n'avons pas pu authentifier votre moyen de paiement. Veuillez choisir un moyen différent et réessayer", + "UPDATE_PAYMENT_METHOD": "Mise à jour du moyen de paiement", + "MONTHLY": "Mensuel", + "YEARLY": "Annuel", + "UPDATE_SUBSCRIPTION_MESSAGE": "Êtes-vous certains de vouloir changer de plan?", + "UPDATE_SUBSCRIPTION": "Changer de plan", + "CANCEL_SUBSCRIPTION": "Annuler l'abonnement", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Toutes vos données seront supprimées de nos serveurs à la fin de cette période d'abonnement.</p><p>Voulez-vous vraiment annuler votre abonnement?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "Êtes-vous sûr de vouloir annuler votre abonnement ", + "SUBSCRIPTION_CANCEL_FAILED": "Échec lors de l'annulation de l'abonnement", + "SUBSCRIPTION_CANCEL_SUCCESS": "Votre abonnement a bien été annulé", + "REACTIVATE_SUBSCRIPTION": "Réactiver l'abonnement", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Une fois réactivée, vous serrez facturé de {{val, datetime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Votre abonnement est bien activé ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Échec lors de la réactivation de l'abonnement", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Merci", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Annuler l'abonnement mobile", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Veuillez annuler votre abonnement depuis l'appli mobile pour activer un abonnement ici", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Veuillez nous contacter à <a>{{emailID}}</a> pour gérer votre abonnement", + "RENAME": "Renommer", + "RENAME_FILE": "Renommer le fichier", + "RENAME_COLLECTION": "Renommer l'album", + "DELETE_COLLECTION_TITLE": "Supprimer l'album?", + "DELETE_COLLECTION": "Supprimer l'album", + "DELETE_COLLECTION_MESSAGE": "Supprimer aussi les photos (et vidéos) présentes dans cet album depuis <a>tous</a> les autres albums dont ils font partie?", + "DELETE_PHOTOS": "Supprimer des photos", + "KEEP_PHOTOS": "Conserver des photos", + "SHARE": "Partager", + "SHARE_COLLECTION": "Partager l'album", + "SHAREES": "Partager avec", + "SHARE_WITH_SELF": "Oups, vous ne pouvez pas partager avec vous-même", + "ALREADY_SHARED": "Oups, vous partager déjà cela avec {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Partage d'album non autorisé", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Le partage est désactivé pour les comptes gratuits", + "DOWNLOAD_COLLECTION": "Télécharger l'album", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Êtes-vous certains de vouloir télécharger l'album complet?</p><p>Tous les fichiers seront mis en file d'attente pour un téléchargement fractionné</p>", + "CREATE_ALBUM_FAILED": "Échec de création de l'album , veuillez réessayer", + "SEARCH": "Recherche", + "SEARCH_RESULTS": "Résultats de la recherche", + "NO_RESULTS": "Aucun résultat trouvé", + "SEARCH_HINT": "Recherche d'albums, dates, descriptions, ...", + "SEARCH_TYPE": { + "COLLECTION": "l'album", + "LOCATION": "Emplacement", + "CITY": "Adresse", + "DATE": "Date", + "FILE_NAME": "Nom de fichier", + "THING": "Chose", + "FILE_CAPTION": "Description", + "FILE_TYPE": "Type de fichier", + "CLIP": "Magique" + }, + "photos_count_zero": "Pas de souvenirs", + "photos_count_one": "1 souvenir", + "photos_count_other": "{{count}} souvenirs", + "TERMS_AND_CONDITIONS": "J'accepte les <a>conditions</a> et la <b>politique de confidentialité</b>", + "ADD_TO_COLLECTION": "Ajouter à l'album", + "SELECTED": "Sélectionné", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Cette vidéo ne peut pas être lue sur votre navigateur", + "PEOPLE": "Visages", + "INDEXING_SCHEDULED": "L'indexation est planifiée...", + "ANALYZING_PHOTOS": "analyse des nouvelles photos {{indexStatus.nSyncedFiles}} sur {{indexStatus.nTotalFiles}} effectué)...", + "INDEXING_PEOPLE": "indexation des visages dans {{indexStatus.nSyncedFiles}} photos...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles}} photos indexées", + "UNIDENTIFIED_FACES": "visages non-identifiés", + "OBJECTS": "objets", + "TEXT": "texte", + "INFO": "Info ", + "INFO_OPTION": "Info (I)", + "FILE_NAME": "Nom de fichier", + "CAPTION_PLACEHOLDER": "Ajouter une description", + "LOCATION": "Emplacement", + "SHOW_ON_MAP": "Visualiser sur OpenStreetMap", + "MAP": "Carte", + "MAP_SETTINGS": "Paramètres de la carte", + "ENABLE_MAPS": "Activer la carte?", + "ENABLE_MAP": "Activer la carte", + "DISABLE_MAPS": "Désactiver la carte?", + "ENABLE_MAP_DESCRIPTION": "<p>Cette fonction affiche vos photos sur une carte du monde.</p> <p>La carte est hébergée par <a>OpenStreetMap</a>, et les emplacements exacts de vos photos ne sont jamais partagés.</p> <p>Vous pouvez désactiver cette fonction à tout moment dans des paramètres.</p>", + "DISABLE_MAP_DESCRIPTION": "<p>Cette fonction désactive l'affichage de vos photos sur une carte du monde.</p> <p>Vous pouvez activer cette fonction à tout moment dans les Paramètres.</p>", + "DISABLE_MAP": "Désactiver la carte", + "DETAILS": "Détails", + "VIEW_EXIF": "Visualiser toutes les données EXIF", + "NO_EXIF": "Aucune donnée EXIF", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Double authentification", + "TWO_FACTOR_AUTHENTICATION": "Authentification double-facteur", + "TWO_FACTOR_QR_INSTRUCTION": "Scannez le QRCode ci-dessous avec une appli d'authentification", + "ENTER_CODE_MANUALLY": "Saisir le code manuellement", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Veuillez saisir ce code dans votre appli d'authentification", + "SCAN_QR_CODE": "Scannez le QRCode de préférence", + "ENABLE_TWO_FACTOR": "Activer la double-authentification", + "ENABLE": "Activer", + "LOST_DEVICE": "Perte de l'appareil identificateur", + "INCORRECT_CODE": "Code non valide", + "TWO_FACTOR_INFO": "Rajoutez une couche de sécurité supplémentaire afin de pas utiliser simplement votre e-mail et mot de passe pour vous connecter à votre compte", + "DISABLE_TWO_FACTOR_LABEL": "Désactiver la double-authentification", + "UPDATE_TWO_FACTOR_LABEL": "Mise à jour de votre appareil identificateur", + "DISABLE": "Désactiver", + "RECONFIGURE": "Reconfigurer", + "UPDATE_TWO_FACTOR": "Mise à jour de la double-authentification", + "UPDATE_TWO_FACTOR_MESSAGE": "Continuer annulera tous les identificateurs précédemment configurés", + "UPDATE": "Mise à jour", + "DISABLE_TWO_FACTOR": "Désactiver la double-authentification", + "DISABLE_TWO_FACTOR_MESSAGE": "Êtes-vous certains de vouloir désactiver la double-authentification", + "TWO_FACTOR_DISABLE_FAILED": "Échec de désactivation de la double-authentification, veuillez réessayer", + "EXPORT_DATA": "Exporter les données", + "SELECT_FOLDER": "Sélectionner un dossier", + "DESTINATION": "Destination", + "START": "Démarrer", + "LAST_EXPORT_TIME": "Horaire du dernier export", + "EXPORT_AGAIN": "Resynchro", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Stockage local non accessible", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Votre navigateur ou un complément bloque ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.", + "SEND_OTT": "Envoyer l'OTP", + "EMAIl_ALREADY_OWNED": "Cet e-mail est déjà pris", + "ETAGS_BLOCKED": "<p>Nosu n'avons pas pu charger les fichiers suivants à cause de la configuration de votre navigateur.</p><p>Veuillez désactiver tous les compléments qui pourraient empêcher ente d'utiliser les <code>eTags</code> pour charger de larges fichiers, ou bien utilisez notre <a>appli pour ordinateur</a>pour une meilleure expérience lors des chargements.</p>", + "SKIPPED_VIDEOS_INFO": "<p>Actuellement, nous ne supportons pas l'ajout de videos via des liens publics.</p><p>Pour partager des vidéos, veuillez <a>vous connecter à</a>ente et partager en utilisant l'e-mail concerné.</p>", + "LIVE_PHOTOS_DETECTED": "Les fichiers photos et vidéos depuis votre espace Live Photos ont été fusionnés en un seul fichier", + "RETRY_FAILED": "Réessayer les chargements ayant échoués", + "FAILED_UPLOADS": "Chargements échoués ", + "SKIPPED_FILES": "Chargements ignorés", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Échec de création d'une miniature", + "UNSUPPORTED_FILES": "Fichiers non supportés", + "SUCCESSFUL_UPLOADS": "Chargements réussis", + "SKIPPED_INFO": "Ignorés car il y a des fichiers avec des noms identiques dans le même album", + "UNSUPPORTED_INFO": "ente ne supporte pas encore ces formats de fichiers", + "BLOCKED_UPLOADS": "Chargements bloqués", + "SKIPPED_VIDEOS": "Vidéos ignorées", + "INPROGRESS_METADATA_EXTRACTION": "En cours", + "INPROGRESS_UPLOADS": "Chargements en cours", + "TOO_LARGE_UPLOADS": "Gros fichiers", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Stockage insuffisant", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Ces fichiers n'ont pas été chargés car ils dépassent la taille maximale de votre plan de stockage", + "TOO_LARGE_INFO": "Ces fichiers n'ont pas été chargés car ils dépassent notre taille limite par fichier", + "THUMBNAIL_GENERATION_FAILED_INFO": "Ces fichiers sont bien chargés, mais nous ne pouvons pas créer de miniatures pour eux.", + "UPLOAD_TO_COLLECTION": "Charger dans l'album", + "UNCATEGORIZED": "Aucune catégorie", + "ARCHIVE": "Archiver", + "FAVORITES": "Favoris", + "ARCHIVE_COLLECTION": "Archiver l'album", + "ARCHIVE_SECTION_NAME": "Archivé", + "ALL_SECTION_NAME": "Tous", + "MOVE_TO_COLLECTION": "Déplacer vers l'album", + "UNARCHIVE": "Désarchiver", + "UNARCHIVE_COLLECTION": "Désarchiver l'album", + "HIDE_COLLECTION": "Masquer l'album", + "UNHIDE_COLLECTION": "Dévoiler l'album", + "MOVE": "Déplacer", + "ADD": "Ajouter", + "REMOVE": "Retirer", + "YES_REMOVE": "Oui, retirer", + "REMOVE_FROM_COLLECTION": "Retirer de l'album", + "TRASH": "Corbeille", + "MOVE_TO_TRASH": "Déplacer vers la corbeille", + "TRASH_FILES_MESSAGE": "Les fichiers sélectionnés seront retirés de tous les albums puis déplacés dans la corbeille.", + "TRASH_FILE_MESSAGE": "Le fichier sera retiré de tous les albums puis déplacé dans la corbeille.", + "DELETE_PERMANENTLY": "Supprimer définitivement", + "RESTORE": "Restaurer", + "RESTORE_TO_COLLECTION": "Restaurer vers l'album", + "EMPTY_TRASH": "Corbeille vide", + "EMPTY_TRASH_TITLE": "Vider la corbeille ?", + "EMPTY_TRASH_MESSAGE": "Ces fichiers seront définitivement supprimés de votre compte ente.", + "LEAVE_SHARED_ALBUM": "Oui, quitter", + "LEAVE_ALBUM": "Quitter l'album", + "LEAVE_SHARED_ALBUM_TITLE": "Quitter l'album partagé?", + "LEAVE_SHARED_ALBUM_MESSAGE": "Vous allez quitter cet album, il ne sera plus visible pour vous.", + "NOT_FILE_OWNER": "Vous ne pouvez pas supprimer les fichiers d'un album partagé", + "CONFIRM_SELF_REMOVE_MESSAGE": "Choisir les objets qui seront retirés de cet album. Ceux qui sont présents uniquement dans cet album seront déplacés comme hors catégorie.", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Certains des objets que vous êtes en train de retirer ont été ajoutés par d'autres personnes, vous perdrez l'accès vers ces objets.", + "SORT_BY_CREATION_TIME_ASCENDING": "Plus anciens", + "SORT_BY_UPDATION_TIME_DESCENDING": "Dernière mise à jour", + "SORT_BY_NAME": "Nom", + "COMPRESS_THUMBNAILS": "Compresser les miniatures", + "THUMBNAIL_REPLACED": "Les miniatures sont compressées", + "FIX_THUMBNAIL": "Compresser", + "FIX_THUMBNAIL_LATER": "Compresser plus tard", + "REPLACE_THUMBNAIL_NOT_STARTED": "Certaines miniatures de vidéos peuvent être compressées pour gagner de la place. Voulez-vous que ente les compresse?", + "REPLACE_THUMBNAIL_COMPLETED": "Toutes les miniatures ont été compressées", + "REPLACE_THUMBNAIL_NOOP": "Vous n'avez aucune miniature qui peut être encore plus compressée", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Impossible de compresser certaines miniatures, veuillez réessayer", + "FIX_CREATION_TIME": "Réajuster l'heure", + "FIX_CREATION_TIME_IN_PROGRESS": "Réajustement de l'heure", + "CREATION_TIME_UPDATED": "L'heure du fichier a été réajustée", + "UPDATE_CREATION_TIME_NOT_STARTED": "Sélectionnez l'option que vous souhaitez utiliser", + "UPDATE_CREATION_TIME_COMPLETED": "Mise à jour effectuée pour tous les fichiers", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "L'heure du fichier n'a pas été mise à jour pour certains fichiers, veuillez réessayer", + "CAPTION_CHARACTER_LIMIT": "5000 caractères max", + "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", + "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", + "METADATA_DATE": "EXIF:MetadataDate", + "CUSTOM_TIME": "Heure personnalisée", + "REOPEN_PLAN_SELECTOR_MODAL": "Rouvrir les plans", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Échec pour rouvrir les plans", + "INSTALL": "Installer", + "SHARING_DETAILS": "Détails du partage", + "MODIFY_SHARING": "Modifier le partage", + "ADD_COLLABORATORS": "Ajouter des collaborateurs", + "ADD_NEW_EMAIL": "Ajouter un nouvel email", + "shared_with_people_zero": "Partager avec des personnes spécifiques", + "shared_with_people_one": "Partagé avec 1 personne", + "shared_with_people_other": "Partagé avec {{count, number}} personnes", + "participants_zero": "Aucun participant", + "participants_one": "1 participant", + "participants_other": "{{count, number}} participants", + "ADD_VIEWERS": "Ajouter un observateur", + "PARTICIPANTS": "Participants", + "CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} ne pourra plus ajouter de photos à l'album</p> <p>Il pourra toujours supprimer les photos qu'il a ajoutées</p>", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} pourra ajouter des photos à l'album", + "CONVERT_TO_VIEWER": "Oui, convertir en observateur", + "CONVERT_TO_COLLABORATOR": "Oui, convertir en collaborateur", + "CHANGE_PERMISSION": "Modifier la permission?", + "REMOVE_PARTICIPANT": "Retirer?", + "CONFIRM_REMOVE": "Oui, supprimer", + "MANAGE": "Gérer", + "ADDED_AS": "Ajouté comme", + "COLLABORATOR_RIGHTS": "Les collaborateurs peuvent ajouter des photos et des vidéos à l'album partagé", + "REMOVE_PARTICIPANT_HEAD": "Supprimer le participant", + "OWNER": "Propriétaire", + "COLLABORATORS": "Collaborateurs", + "ADD_MORE": "Ajouter plus", + "VIEWERS": "Visionneurs", + "OR_ADD_EXISTING": "ou sélectionner un fichier existant", + "REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} sera supprimé de l'album</p> <p>Toutes les photos ajoutées par cette personne seront également supprimées de l'album</p>", + "NOT_FOUND": "404 - non trouvé", + "LINK_EXPIRED": "Lien expiré", + "LINK_EXPIRED_MESSAGE": "Ce lien à soit expiré soit est supprimé!", + "MANAGE_LINK": "Gérer le lien", + "LINK_TOO_MANY_REQUESTS": "Désolé, cet album a été consulté sur trop d'appareils !", + "FILE_DOWNLOAD": "Autoriser les téléchargements", + "LINK_PASSWORD_LOCK": "Verrou par mot de passe", + "PUBLIC_COLLECT": "Autoriser l'ajout de photos", + "LINK_DEVICE_LIMIT": "Limite d'appareil", + "NO_DEVICE_LIMIT": "Aucune", + "LINK_EXPIRY": "Expiration du lien", + "NEVER": "Jamais", + "DISABLE_FILE_DOWNLOAD": "Désactiver le téléchargement", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>Êtes-vous certains de vouloir désactiver le bouton de téléchargement pour les fichiers?</p><p>Ceux qui les visualisent pourront tout de même faire des captures d'écrans ou sauvegarder une copie de vos photos en utilisant des outils externes.</p>", + "MALICIOUS_CONTENT": "Contient du contenu malveillant", + "COPYRIGHT": "Enfreint les droits d'une personne que je réprésente", + "SHARED_USING": "Partagé en utilisant ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "Utilisez le code <strong>{{referralCode}}</strong> pour obtenir 10 Go gratuits", + "LIVE": "LIVE", + "DISABLE_PASSWORD": "Désactiver le verrouillage par mot de passe", + "DISABLE_PASSWORD_MESSAGE": "Êtes-vous certains de vouloir désactiver le verrouillage par mot de passe ?", + "PASSWORD_LOCK": "Mot de passe verrou", + "LOCK": "Verrouiller", + "DOWNLOAD_UPLOAD_LOGS": "Journaux de débugs", + "UPLOAD_FILES": "Fichier", + "UPLOAD_DIRS": "Dossier", + "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", + "DEDUPLICATE_FILES": "Déduplication de fichiers", + "AUTHENTICATOR_SECTION": "Authentificateur", + "NO_DUPLICATES_FOUND": "Vous n'avez aucun fichier dédupliqué pouvant être nettoyé", + "CLUB_BY_CAPTURE_TIME": "Durée de la capture par club", + "FILES": "Fichiers", + "EACH": "Chacun", + "DEDUPLICATE_BASED_ON_SIZE": "Les fichiers suivants ont été clubbed, basé sur leurs tailles, veuillez corriger et supprimer les objets que vous pensez être dupliqués", + "STOP_ALL_UPLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?", + "STOP_UPLOADS_HEADER": "Arrêter les chargements ?", + "YES_STOP_UPLOADS": "Oui, arrêter tout", + "STOP_DOWNLOADS_HEADER": "Arrêter le téléchargement ?", + "YES_STOP_DOWNLOADS": "Oui, arrêter les téléchargements", + "STOP_ALL_DOWNLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?", + "albums_one": "1 album", + "albums_other": "{{count}} albums", + "ALL_ALBUMS": "Tous les albums", + "ALBUMS": "Albums", + "ALL_HIDDEN_ALBUMS": "Tous les albums masqués", + "HIDDEN_ALBUMS": "Albums masqués", + "HIDDEN_ITEMS": "Éléments masqués", + "HIDDEN_ITEMS_SECTION_NAME": "Éléments masqués", + "ENTER_TWO_FACTOR_OTP": "Saisir le code à 6 caractères de votre appli d'authentification.", + "CREATE_ACCOUNT": "Créer un compte", + "COPIED": "Copié", + "CANVAS_BLOCKED_TITLE": "Impossible de créer une miniature", + "CANVAS_BLOCKED_MESSAGE": "<p>Il semblerait que votre navigateur ait désactivé l'accès au canevas, qui est nécessaire pour créer les miniatures de vos photos </p><p>Veuillez activer l'accès au canevas du navigateur, ou consulter notre appli pour ordinateur</p></>", + "WATCH_FOLDERS": "Voir les dossiers", + "UPGRADE_NOW": "Mettre à niveau maintenant", + "RENEW_NOW": "Renouveler maintenant", + "STORAGE": "Stockage", + "USED": "utilisé", + "YOU": "Vous", + "FAMILY": "Famille", + "FREE": "gratuit", + "OF": "de", + "WATCHED_FOLDERS": "Voir les dossiers", + "NO_FOLDERS_ADDED": "Aucun dossiers d'ajouté!", + "FOLDERS_AUTOMATICALLY_MONITORED": "Les dossiers que vous ajoutez ici seront supervisés automatiquement", + "UPLOAD_NEW_FILES_TO_ENTE": "Charger de nouveaux fichiers sur ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Retirer de ente les fichiers supprimés", + "ADD_FOLDER": "Ajouter un dossier", + "STOP_WATCHING": "Arrêter de voir", + "STOP_WATCHING_FOLDER": "Arrêter de voir le dossier?", + "STOP_WATCHING_DIALOG_MESSAGE": "Vos fichiers existants ne seront pas supprimés, mais ente arrêtera automatiquement de mettre à jour le lien de l'album à chaque changements sur ce dossier.", + "YES_STOP": "Oui, arrêter", + "MONTH_SHORT": "mo", + "YEAR": "année", + "FAMILY_PLAN": "Plan famille", + "DOWNLOAD_LOGS": "Télécharger les logs", + "DOWNLOAD_LOGS_MESSAGE": "<p>Cela va télécharger les journaux de débug, que vous pourrez nosu envoyer par e-mail pour nous aider à résoudre votre problàme .</p><p>Veuillez noter que les noms de fichiers seront inclus .</p>", + "CHANGE_FOLDER": "Modifier le dossier", + "TWO_MONTHS_FREE": "Obtenir 2 mois gratuits sur les plans annuels", + "GB": "Go", + "POPULAR": "Populaire", + "FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite", + "FREE_PLAN_DESCRIPTION": "1 Go pour 1 an", + "CURRENT_USAGE": "L'utilisation actuelle est de <strong>{{usage}}</strong>", + "WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à ente sur votre ordinateur, ou télécharger l'appli ente mobile/ordinateur.", + "DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre ente", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "<p> Vos données chargées seront programmées pour suppression, et votre comptre sera supprimé définitivement .</p><p>Cette action n'est pas reversible.</p>", + "AUTHENTICATE": "Authentification", + "UPLOADED_TO_SINGLE_COLLECTION": "Chargé dans une seule collection", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "Chargé dans des collections séparées", + "NEVERMIND": "Peu-importe", + "UPDATE_AVAILABLE": "Une mise à jour est disponible", + "UPDATE_INSTALLABLE_MESSAGE": "Une nouvelle version de ente est prête à être installée.", + "INSTALL_NOW": "Installer maintenant", + "INSTALL_ON_NEXT_LAUNCH": "Installer au prochain démarrage", + "UPDATE_AVAILABLE_MESSAGE": "Une nouvelle version de ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", + "DOWNLOAD_AND_INSTALL": "Télécharger et installer", + "IGNORE_THIS_VERSION": "Ignorer cette version", + "TODAY": "Aujourd'hui", + "YESTERDAY": "Hier", + "NAME_PLACEHOLDER": "Nom...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Impossible de créer des albums depuis un mix fichier/dossier", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Vous avez glissé déposé un mélange de fichiers et dossiers.</p><p>Veuillez sélectionner soit uniquement des fichiers, ou des dossiers lors du choix d'options pour créer des albums séparés</p>", + "CHOSE_THEME": "Choisir un thème", + "ML_SEARCH": "ML search (beta)", + "ENABLE_ML_SEARCH_DESCRIPTION": "<p>Ceci activera l'apprentissage automatique sur l'appareil et la recherche faciale qui commencera à analyser vos photos chargées.</p><p>Pour la première exécution après la connexion ou l'activation de cette fonctionnalité, cela téléchargera toutes les images sur l'appareil local pour les analyser. Veuillez donc activer ceci uniquement si vous avez de la bande passante et le traitement local de toutes les images dans votre photothèque.</p><p>Si c'est la première fois que vous activez ceci, nous vous demanderons également la permission de traiter les données faciales.</p>", + "ML_MORE_DETAILS": "Plus de détails", + "ENABLE_FACE_SEARCH": "Activer la recherche faciale", + "ENABLE_FACE_SEARCH_TITLE": "Activer la recherche faciale ?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "<p>If you enable face search, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.</p><p><a>Please click here for more details about this feature in our privacy policy</a></p>", + "DISABLE_BETA": "Désactiver la bêta", + "DISABLE_FACE_SEARCH": "Désactiver la recherche faciale", + "DISABLE_FACE_SEARCH_TITLE": "Désactiver la recherche faciale ?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente will stop processing face geometry, and will also disable ML search (beta)</p><p>You can reenable face search again if you wish, so this operation is safe</p>", + "ADVANCED": "Avancé", + "FACE_SEARCH_CONFIRMATION": "Je comprends, et je souhaite permettre à ente de traiter la géométrie faciale", + "LABS": "Labs", + "YOURS": "Le vôtre", + "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", + "PREFERENCES": "Préférences", + "LANGUAGE": "Langue", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Dossier d'export invalide", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p> Le dossier d'export que vous avez sélectionné n'existe pas </p><p>Veuillez sélectionner un dossier valide</p>", + "SUBSCRIPTION_VERIFICATION_ERROR": "Échec de la vérification de l'abonnement", + "STORAGE_UNITS": { + "B": "o", + "KB": "Ko", + "MB": "Mo", + "GB": "Go", + "TB": "To" + }, + "AFTER_TIME": { + "HOUR": "dans une heure", + "DAY": "dans un jour", + "WEEK": "dans une semaine", + "MONTH": "dans un mois", + "YEAR": "dans un an" + }, + "COPY_LINK": "Copier le lien", + "DONE": "Terminé", + "LINK_SHARE_TITLE": "Ou partager un lien", + "REMOVE_LINK": "Supprimer le lien", + "CREATE_PUBLIC_SHARING": "Créer un lien public", + "PUBLIC_LINK_CREATED": "Lien public créé", + "PUBLIC_LINK_ENABLED": "Lien public activé", + "COLLECT_PHOTOS": "Récupérer les photos", + "PUBLIC_COLLECT_SUBTEXT": "Autoriser les personnes ayant le lien d'ajouter des photos à l'album partagé.", + "STOP_EXPORT": "Stop", + "EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> fichiers exportés", + "MIGRATING_EXPORT": "Préparations...", + "RENAMING_COLLECTION_FOLDERS": "Renommage des dossiers de l'album en cours...", + "TRASHING_DELETED_FILES": "Mise à la corbeille des fichiers supprimés...", + "TRASHING_DELETED_COLLECTIONS": "Mise à la corbeille des albums supprimés...", + "EXPORT_NOTIFICATION": { + "START": "L'export a démarré", + "IN_PROGRESS": "Un export est déjà en cours", + "FINISH": "Export terminé", + "UP_TO_DATE": "Aucun nouveau fichier à exporter" + }, + "CONTINUOUS_EXPORT": "Synchronisation en continu", + "TOTAL_ITEMS": "Total d'objets", + "PENDING_ITEMS": "Objets en attente", + "EXPORT_STARTING": "Démarrage de l'export...", + "DELETE_ACCOUNT_REASON_LABEL": "Quelle est la raison principale de la suppression de votre compte ?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Choisir une raison", + "DELETE_REASON": { + "MISSING_FEATURE": "Il manque une fonctionnalité essentielle dont j'ai besoin", + "BROKEN_BEHAVIOR": "L'application ou une certaine fonctionnalité ne se comporte pas comme je pense qu'elle devrait", + "FOUND_ANOTHER_SERVICE": "J'ai trouvé un autre service que je préfère", + "NOT_LISTED": "Ma raison n'est pas listée" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "Nous sommes désolés de vous voir partir. Expliquez-nous les raisons de votre départ pour que nous puissions nous améliorer.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Vos commentaires", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Oui, je veux supprimer définitivement ce compte et toutes ses données", + "CONFIRM_DELETE_ACCOUNT": "Confirmer la suppression du compte", + "FEEDBACK_REQUIRED": "Merci de nous aider avec cette information", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Qu'est-ce que l'autre service fait de mieux ?", + "RECOVER_TWO_FACTOR": "Récupérer la double-authentification", + "at": "à", + "AUTH_NEXT": "suivant", + "AUTH_DOWNLOAD_MOBILE_APP": "Téléchargez notre application mobile pour gérer vos secrets", + "HIDDEN": "Masqué", + "HIDE": "Masquer", + "UNHIDE": "Dévoiler", + "UNHIDE_TO_COLLECTION": "Afficher dans l'album", + "SORT_BY": "Trier par", + "NEWEST_FIRST": "Plus récent en premier", + "OLDEST_FIRST": "Plus ancien en premier", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Ce fichier n'a pas pu être aperçu. Cliquez ici pour télécharger l'original.", + "SELECT_COLLECTION": "Sélectionner album", + "PIN_ALBUM": "Épingler l'album", + "UNPIN_ALBUM": "Désépingler l'album", + "DOWNLOAD_COMPLETE": "Téléchargement terminé", + "DOWNLOADING_COLLECTION": "Téléchargement de {{name}}", + "DOWNLOAD_FAILED": "Échec du téléchargement", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} fichiers", + "CHRISTMAS": "Noël", + "CHRISTMAS_EVE": "Réveillon de Noël", + "NEW_YEAR": "Nouvel an", + "NEW_YEAR_EVE": "Réveillon de Nouvel An", + "IMAGE": "Image", + "VIDEO": "Vidéo", + "LIVE_PHOTO": "Photos en direct", + "CONVERT": "Convertir", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Êtes-vous sûr de vouloir fermer l'éditeur ?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Téléchargez votre image modifiée ou enregistrez une copie sur ente pour maintenir vos modifications.", + "BRIGHTNESS": "Luminosité", + "CONTRAST": "Contraste", + "SATURATION": "Saturation", + "BLUR": "Flou", + "INVERT_COLORS": "Inverser les couleurs", + "ASPECT_RATIO": "Ratio de l'image", + "SQUARE": "Carré", + "ROTATE_LEFT": "Pivoter vers la gauche", + "ROTATE_RIGHT": "Pivoter vers la droite", + "FLIP_VERTICALLY": "Basculer verticalement", + "FLIP_HORIZONTALLY": "Retourner horizontalement", + "DOWNLOAD_EDITED": "Téléchargement modifié", + "SAVE_A_COPY_TO_ENTE": "Enregistrer une copie dans ente", + "RESTORE_ORIGINAL": "Restaurer l'original", + "TRANSFORM": "Transformer", + "COLORS": "Couleurs", + "FLIP": "Retourner", + "ROTATION": "Rotation", + "RESET": "Réinitialiser", + "PHOTO_EDITOR": "Éditeur de photos", + "FASTER_UPLOAD": "Chargements plus rapides", + "FASTER_UPLOAD_DESCRIPTION": "Router les chargements vers les serveurs à proximité", + "MAGIC_SEARCH_STATUS": "Statut de la recherche magique", + "INDEXED_ITEMS": "Éléments indexés", + "CAST_ALBUM_TO_TV": "Jouer l'album sur la TV", + "ENTER_CAST_PIN_CODE": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.", + "PAIR_DEVICE_TO_TV": "Associer les appareils", + "TV_NOT_FOUND": "TV introuvable. Avez-vous entré le code PIN correctement ?", + "AUTO_CAST_PAIR": "Paire automatique", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "La paire automatique nécessite la connexion aux serveurs Google et ne fonctionne qu'avec les appareils pris en charge par Chromecast. Google ne recevra pas de données sensibles, telles que vos photos.", + "PAIR_WITH_PIN": "Associer avec le code PIN", + "CHOOSE_DEVICE_FROM_BROWSER": "Choisissez un périphérique compatible avec la caste à partir de la fenêtre pop-up du navigateur.", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "L'association avec le code PIN fonctionne pour tout appareil grand écran sur lequel vous voulez lire votre album.", + "VISIT_CAST_ENTE_IO": "Visitez cast.ente.io sur l'appareil que vous voulez associer.", + "CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.", + "CACHE_DIRECTORY": "Dossier du cache", + "FREEHAND": "Main levée", + "APPLY_CROP": "Appliquer le recadrage", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.", + "PASSKEYS": "Clés d'accès", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/it-IT/translation.json b/web/apps/cast/public/locales/it-IT/translation.json new file mode 100644 index 000000000..ae450e5fe --- /dev/null +++ b/web/apps/cast/public/locales/it-IT/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Backup privati</div><div>dei tuoi ricordi</div>", + "HERO_SLIDE_1": "Crittografia end-to-end", + "HERO_SLIDE_2_TITLE": "<div>Salvati in modo sicuro</div><div>in un rifugio antiatomico</div>", + "HERO_SLIDE_2": "Progettato per sopravvivere", + "HERO_SLIDE_3_TITLE": "<div>Disponibile</div><div> ovunque</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Desktop", + "LOGIN": "Accedi", + "SIGN_UP": "Registrati", + "NEW_USER": "Nuovo utente", + "EXISTING_USER": "Accedi", + "ENTER_NAME": "Inserisci il nome", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Aggiungi un nome in modo che i tuoi amici sappiano chi ringraziare per queste fantastiche foto!", + "ENTER_EMAIL": "Inserisci l'indirizzo email", + "EMAIL_ERROR": "Inserisci un indirizzo email valido", + "REQUIRED": "Campo obbligatorio", + "EMAIL_SENT": "Codice di verifica inviato a <a>{{email}}</a>", + "CHECK_INBOX": "Controlla la tua casella di posta (e lo spam) per completare la verifica", + "ENTER_OTT": "Codice di verifica", + "RESEND_MAIL": "Reinvia codice", + "VERIFY": "Verifica", + "UNKNOWN_ERROR": "Qualcosa è andato storto, per favore riprova", + "INVALID_CODE": "Codice di verifica non valido", + "EXPIRED_CODE": "Il tuo codice di verifica è scaduto", + "SENDING": "Invio in corso...", + "SENT": "Inviato!", + "PASSWORD": "Password", + "LINK_PASSWORD": "Inserisci la password per sbloccare l'album", + "RETURN_PASSPHRASE_HINT": "Password", + "SET_PASSPHRASE": "Imposta una password", + "VERIFY_PASSPHRASE": "Accedi", + "INCORRECT_PASSPHRASE": "Password sbagliata", + "ENTER_ENC_PASSPHRASE": "Inserisci una password per crittografare i tuoi dati", + "PASSPHRASE_DISCLAIMER": "Non memorizziamo la tua password, quindi se la dimentichi, <strong>non saremo in grado di aiutarti </strong>a recuperare i tuoi dati senza una chiave di recupero.", + "WELCOME_TO_ENTE_HEADING": "Benvenuto su <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Archiviazione e condivisione di foto crittografate end-to-end", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Dove vivono le tue migliori foto", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generazione delle chiavi di crittografia...", + "PASSPHRASE_HINT": "Password", + "CONFIRM_PASSPHRASE": "Conferma la password", + "REFERRAL_CODE_HINT": "Come hai conosciuto Ente? (opzionale)", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "Le password non corrispondono", + "CREATE_COLLECTION": "Nuovo album", + "ENTER_ALBUM_NAME": "Nome album", + "CLOSE_OPTION": "Chiudi (Esc)", + "ENTER_FILE_NAME": "Nome del file", + "CLOSE": "Chiudi", + "NO": "No", + "NOTHING_HERE": "Nulla da vedere qui! 👀", + "UPLOAD": "Carica", + "IMPORT": "Importa", + "ADD_PHOTOS": "Aggiungi foto", + "ADD_MORE_PHOTOS": "Aggiungi altre foto", + "add_photos_one": "Aggiungi elemento", + "add_photos_other": "Aggiungi {{count, number}} elementi", + "SELECT_PHOTOS": "Seleziona foto", + "FILE_UPLOAD": "Carica file", + "UPLOAD_STAGE_MESSAGE": { + "0": "Preparazione all'upload", + "1": "Lettura dei file metadati di google", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} file metadati estratti", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} file salvati", + "4": "Annullamento dei caricamenti rimanenti", + "5": "Backup completato" + }, + "FILE_NOT_UPLOADED_LIST": "I seguenti file non sono stati caricati", + "SUBSCRIPTION_EXPIRED": "Abbonamento scaduto", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Il tuo abbonamento è scaduto, per favore <a>rinnova</a>", + "STORAGE_QUOTA_EXCEEDED": "Limite d'archiviazione superato", + "INITIAL_LOAD_DELAY_WARNING": "Il primo caricamento potrebbe richiedere del tempo", + "USER_DOES_NOT_EXIST": "Purtroppo non abbiamo trovato nessun account con quell'indirizzo e-mail", + "NO_ACCOUNT": "Non ho un account", + "ACCOUNT_EXISTS": "Ho già un account", + "CREATE": "Crea", + "DOWNLOAD": "Scarica", + "DOWNLOAD_OPTION": "Scarica (D)", + "DOWNLOAD_FAVORITES": "Scarica i preferiti", + "DOWNLOAD_UNCATEGORIZED": "Scarica i file senza categoria", + "DOWNLOAD_HIDDEN_ITEMS": "Scarica gli elementi nascosti", + "COPY_OPTION": "Copia come PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Attiva/disattiva schermo intero (F)", + "ZOOM_IN_OUT": "Zoom in/out", + "PREVIOUS": "Precedente (←)", + "NEXT": "Successivo (→)", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "Carica la tua prima foto", + "IMPORT_YOUR_FOLDERS": "Importa una cartella", + "UPLOAD_DROPZONE_MESSAGE": "Rilascia per eseguire il backup dei file", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Rilascia per aggiungere la cartella osservata", + "TRASH_FILES_TITLE": "Elimina file?", + "TRASH_FILE_TITLE": "Eliminare il file?", + "DELETE_FILES_TITLE": "Eliminare immediatamente?", + "DELETE_FILES_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "DELETE": "Cancella", + "DELETE_OPTION": "Cancella (DEL)", + "FAVORITE_OPTION": "Preferito (L)", + "UNFAVORITE_OPTION": "Rimuovi dai preferiti (L)", + "MULTI_FOLDER_UPLOAD": "Selezionate più cartelle", + "UPLOAD_STRATEGY_CHOICE": "Vuoi caricarli in", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Un album singolo", + "OR": "o", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Album separati", + "SESSION_EXPIRED_MESSAGE": "La sessione è scaduta. Per continuare, esegui nuovamente l'accesso", + "SESSION_EXPIRED": "Sessione scaduta", + "PASSWORD_GENERATION_FAILED": "Il tuo browser non è stato in grado di generare una chiave forte che soddisfa gli standard di crittografia ente, prova ad usare l'app per dispositivi mobili o un altro browser", + "CHANGE_PASSWORD": "Cambia password", + "GO_BACK": "Torna indietro", + "RECOVERY_KEY": "Chiave di recupero", + "SAVE_LATER": "Fallo più tardi", + "SAVE": "Salva Chiave", + "RECOVERY_KEY_DESCRIPTION": "Se dimentichi la tua password, l'unico modo per recuperare i tuoi dati è con questa chiave.", + "RECOVER_KEY_GENERATION_FAILED": "Impossibile generare il codice di recupero, riprova", + "KEY_NOT_STORED_DISCLAIMER": "Non memorizziamo questa chiave, quindi salvala in un luogo sicuro", + "FORGOT_PASSWORD": "Password dimenticata", + "RECOVER_ACCOUNT": "Recupera account", + "RECOVERY_KEY_HINT": "Chiave di recupero", + "RECOVER": "Recupera", + "NO_RECOVERY_KEY": "Nessuna chiave di recupero?", + "INCORRECT_RECOVERY_KEY": "Chiave di recupero errata", + "SORRY": "Siamo spiacenti", + "NO_RECOVERY_KEY_MESSAGE": "A causa della natura del nostro protocollo di crittografia end-to-end, i tuoi dati non possono essere decifrati senza la tua password o chiave di ripristino", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Per favore invia un'email a <a>{{emailID}}</a> dal tuo indirizzo email registrato", + "CONTACT_SUPPORT": "Contatta il supporto", + "REQUEST_FEATURE": "Richiedi una funzionalità", + "SUPPORT": "Supporto", + "CONFIRM": "Conferma", + "CANCEL": "Annulla", + "LOGOUT": "Disconnettiti", + "DELETE_ACCOUNT": "Elimina account", + "DELETE_ACCOUNT_MESSAGE": "<p>Per favore invia una email a <a>{{emailID}}</a> dal tuo indirizzo email registrato.</p><p>La tua richiesta verrà elaborata entro 72 ore.</p>", + "LOGOUT_MESSAGE": "Sei sicuro di volerti disconnettere?", + "CHANGE_EMAIL": "Cambia email", + "OK": "OK", + "SUCCESS": "Operazione riuscita", + "ERROR": "Errore", + "MESSAGE": "Messaggio", + "INSTALL_MOBILE_APP": "Installa la nostra app <a>Android</a> o <b>iOS</b> per eseguire il backup automatico di tutte le tue foto", + "DOWNLOAD_APP_MESSAGE": "Siamo spiacenti, questa operazione è attualmente supportata solo sulla nostra app desktop", + "DOWNLOAD_APP": "Scarica l'app per desktop", + "EXPORT": "Esporta Dati", + "SUBSCRIPTION": "Abbonamento", + "SUBSCRIBE": "Iscriviti", + "MANAGEMENT_PORTAL": "Gestisci i metodi di pagamento", + "MANAGE_FAMILY_PORTAL": "Gestisci piano famiglia", + "LEAVE_FAMILY_PLAN": "Abbandona il piano famiglia", + "LEAVE": "Lascia", + "LEAVE_FAMILY_CONFIRM": "Sei sicuro di voler uscire dal piano famiglia?", + "CHOOSE_PLAN": "Scegli il tuo piano", + "MANAGE_PLAN": "Gestisci il tuo abbonamento", + "ACTIVE": "Attivo", + "OFFLINE_MSG": "Sei offline, i ricordi memorizzati nella cache vengono mostrati", + "FREE_SUBSCRIPTION_INFO": "Sei sul piano <strong>gratuito</strong> che scade il {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Fai parte di un piano famiglia gestito da", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Si rinnova il {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Termina il {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Il tuo abbonamento verrà annullato il {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Hai superato la quota di archiviazione assegnata, si prega di aggiornare <a></a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>Abbiamo ricevuto il tuo pagamento</p><p>Il tuo abbonamento è valido fino a <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Il tuo acquisto è stato annullato, riprova se vuoi iscriverti", + "SUBSCRIPTION_PURCHASE_FAILED": "Acquisto abbonamento non riuscito, riprova", + "SUBSCRIPTION_UPDATE_FAILED": "L'aggiornamento dell'abbonamento non è riuscito, riprova", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Siamo spiacenti, il pagamento non è andato a buon fine quando abbiamo provato ad addebitare alla sua carta, la preghiamo di aggiornare il suo metodo di pagamento e riprovare", + "STRIPE_AUTHENTICATION_FAILED": "Non siamo in grado di autenticare il tuo metodo di pagamento. Per favore scegli un metodo di pagamento diverso e riprova", + "UPDATE_PAYMENT_METHOD": "Aggiorna metodo di pagamento", + "MONTHLY": "Mensile", + "YEARLY": "Annuale", + "UPDATE_SUBSCRIPTION_MESSAGE": "Sei sicuro di voler cambiare il piano?", + "UPDATE_SUBSCRIPTION": "Cambia piano", + "CANCEL_SUBSCRIPTION": "Annulla abbonamento", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Tutti i tuoi dati saranno cancellati dai nostri server alla fine di questo periodo di fatturazione.</p><p>Sei sicuro di voler annullare il tuo abbonamento?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "Impossibile annullare l'abbonamento", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abbonamento annullato con successo", + "REACTIVATE_SUBSCRIPTION": "Riattiva abbonamento", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Una volta riattivato, ti verrà addebitato il valore di {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Iscrizione attivata con successo ", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Grazie", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Annulla abbonamento mobile", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Per favore contattaci su <a>{{emailID}}</a> per gestire il tuo abbonamento", + "RENAME": "Rinomina", + "RENAME_FILE": "Rinomina file", + "RENAME_COLLECTION": "Rinomina album", + "DELETE_COLLECTION_TITLE": "Eliminare l'album?", + "DELETE_COLLECTION": "Elimina album", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "Elimina foto", + "KEEP_PHOTOS": "Mantieni foto", + "SHARE": "Condividi", + "SHARE_COLLECTION": "Condividi album", + "SHAREES": "Condividi con", + "SHARE_WITH_SELF": "Ops, non puoi condividere a te stesso", + "ALREADY_SHARED": "Ops, lo stai già condividendo con {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Condividere gli album non è consentito", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "La condivisione è disabilitata per gli account free", + "DOWNLOAD_COLLECTION": "Scarica album", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Sei sicuro di volere scaricare l'album interamente?</p><p>Tutti i file saranno messi in coda per il download</p>", + "CREATE_ALBUM_FAILED": "Operazione di creazione dell'album fallita, per favore riprova", + "SEARCH": "Ricerca", + "SEARCH_RESULTS": "Risultati della ricerca", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "Album", + "LOCATION": "Posizione", + "CITY": "Posizione", + "DATE": "Data", + "FILE_NAME": "Nome file", + "THING": "Contenuto", + "FILE_CAPTION": "Descrizione", + "FILE_TYPE": "Tipo del file", + "CLIP": "" + }, + "photos_count_zero": "Nessuna memoria", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "Aggiungi all'album", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Questo video non può essere riprodotto nel tuo browser", + "PEOPLE": "Persone", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "volti non identificati", + "OBJECTS": "", + "TEXT": "testo", + "INFO": "Info ", + "INFO_OPTION": "", + "FILE_NAME": "Nome file", + "CAPTION_PLACEHOLDER": "Aggiungi una descrizione", + "LOCATION": "Posizione", + "SHOW_ON_MAP": "Guarda su OpenStreetMap", + "MAP": "Mappa", + "MAP_SETTINGS": "Impostazioni Mappa", + "ENABLE_MAPS": "Attivare Mappa?", + "ENABLE_MAP": "Attivare mappa", + "DISABLE_MAPS": "Disattivare Mappa?", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Due fattori", + "TWO_FACTOR_AUTHENTICATION": "Autenticazione a due fattori", + "TWO_FACTOR_QR_INSTRUCTION": "Scansiona il codice QR qui sotto con la tua app di autenticazione preferita", + "ENTER_CODE_MANUALLY": "Inserisci il codice manualmente", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Inserisci questo codice nella tua app di autenticazione preferita", + "SCAN_QR_CODE": "Oppure scansiona il codice QR", + "ENABLE_TWO_FACTOR": "Attiva due fattori", + "ENABLE": "Attiva", + "LOST_DEVICE": "", + "INCORRECT_CODE": "Codice errato", + "TWO_FACTOR_INFO": "Aggiungi un ulteriore livello di sicurezza richiedendo più informazioni rispetto a email e password per eseguire l'accesso al tuo account", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "Esporta dati", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "Invia OTP", + "EMAIl_ALREADY_OWNED": "Email già in uso", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "Caricamento fallito ", + "SKIPPED_FILES": "Ignora caricamenti", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "Caricamenti eseguiti con successo", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "Video saltati", + "INPROGRESS_METADATA_EXTRACTION": "In corso", + "INPROGRESS_UPLOADS": "Caricamenti in corso", + "TOO_LARGE_UPLOADS": "File pesanti", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Spazio insufficiente", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Questi file non sono stati caricati perché supererebbero la capacità massima del tuo piano di spazio d'archiviazione", + "TOO_LARGE_INFO": "Questi file non sono stati caricati perché superano il nostro limite di pesantezza di un file", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "Archivio", + "FAVORITES": "Preferiti", + "ARCHIVE_COLLECTION": "Album archiviato", + "ARCHIVE_SECTION_NAME": "Archivio", + "ALL_SECTION_NAME": "Tutto", + "MOVE_TO_COLLECTION": "Sposta nell'album", + "UNARCHIVE": "Rimuovi dall'archivio", + "UNARCHIVE_COLLECTION": "Rimuovi album dall'archivio", + "HIDE_COLLECTION": "Nascondi album", + "UNHIDE_COLLECTION": "Rimuovi album dai nascosti", + "MOVE": "Sposta", + "ADD": "Aggiungi", + "REMOVE": "Rimuovi", + "YES_REMOVE": "Sì, rimuovi", + "REMOVE_FROM_COLLECTION": "Rimuovi dall'album", + "TRASH": "Cestino", + "MOVE_TO_TRASH": "Sposta nel cestino", + "TRASH_FILES_MESSAGE": "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino.", + "TRASH_FILE_MESSAGE": "Il file verrà eliminato da tutti gli album e spostato nel cestino.", + "DELETE_PERMANENTLY": "Elimina definitivamente", + "RESTORE": "Ripristina", + "RESTORE_TO_COLLECTION": "Ripristina nell'album", + "EMPTY_TRASH": "Svuota il cestino", + "EMPTY_TRASH_TITLE": "Vuoi svuotare il cestino?", + "EMPTY_TRASH_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "LEAVE_SHARED_ALBUM": "Sì, esci", + "LEAVE_ALBUM": "Abbandona l'album", + "LEAVE_SHARED_ALBUM_TITLE": "Abbandonare l'album condiviso?", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "Meno recente", + "SORT_BY_UPDATION_TIME_DESCENDING": "Ultimo aggiornamento", + "SORT_BY_NAME": "Nome", + "COMPRESS_THUMBNAILS": "Comprimi miniature", + "THUMBNAIL_REPLACED": "Miniature compresse", + "FIX_THUMBNAIL": "Comprimi", + "FIX_THUMBNAIL_LATER": "Comprimi più tardi", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "Installa", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "Nessun partecipante", + "participants_one": "1 partecipante", + "participants_other": "{{count, number}} partecipanti", + "ADD_VIEWERS": "", + "PARTICIPANTS": "Partecipanti", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "Rimuovere?", + "CONFIRM_REMOVE": "Sì, rimuovi", + "MANAGE": "Gestisci", + "ADDED_AS": "Aggiunto come", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "Rimuovi partecipante", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "404 - non trovato", + "LINK_EXPIRED": "Link scaduto", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "Cartella", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "1 Album", + "albums_other": "{{count, number}} Album", + "ALL_ALBUMS": "Tutti gli Album", + "ALBUMS": "Album", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "Crea account", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "Tu", + "FAMILY": "Famiglia", + "FREE": "gratis", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "Ancora nessuna cartella aggiunta!", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "Cambia Cartella", + "TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali", + "GB": "GB", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "1 GB per 1 anno", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "AUTHENTICATE": "Autenticati", + "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": "Oggi", + "YESTERDAY": "Ieri", + "NAME_PLACEHOLDER": "Nome...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "Seleziona tema", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "Più dettagli", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "Avanzate", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "Sicurezza password: Debole", + "PASSPHRASE_STRENGTH_MODERATE": "Sicurezza password: Moderata", + "PASSPHRASE_STRENGTH_STRONG": "Sicurezza password: Forte", + "PREFERENCES": "", + "LANGUAGE": "Lingua", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "dopo un'ora", + "DAY": "dopo un giorno", + "WEEK": "dopo una settimana", + "MONTH": "dopo un mese", + "YEAR": "dopo un anno" + }, + "COPY_LINK": "Copia link", + "DONE": "Fatto", + "LINK_SHARE_TITLE": "O condividi un link", + "REMOVE_LINK": "Rimuovi link", + "CREATE_PUBLIC_SHARING": "Crea link pubblico", + "PUBLIC_LINK_CREATED": "Link pubblick creato", + "PUBLIC_LINK_ENABLED": "Link pubblico attivato", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Seleziona un motivo", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/ko-KR/translation.json b/web/apps/cast/public/locales/ko-KR/translation.json new file mode 100644 index 000000000..4fbe6c077 --- /dev/null +++ b/web/apps/cast/public/locales/ko-KR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요", + "HERO_SLIDE_1": "종단간 암호화가 기본지원입니다", + "HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다", + "HERO_SLIDE_2": "오랫동안 보존할 수 있도록한 설계", + "HERO_SLIDE_3_TITLE": "<div>어디에서나</div><div> 이용가능</div>", + "HERO_SLIDE_3": "안드로이드, iOS, 웹, 데스크탑", + "LOGIN": "로그인", + "SIGN_UP": "회원가입", + "NEW_USER": "ente의 새소식", + "EXISTING_USER": "기존 사용자", + "ENTER_NAME": "이름 입력", + "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", + "ENTER_EMAIL": "이메일 주소를 입력하세요", + "EMAIL_ERROR": "올바른 이메일을 입력하세요", + "REQUIRED": "필수", + "EMAIL_SENT": "<a>{{email}}</a> 로 인증 코드가 전송되었습니다", + "CHECK_INBOX": "인증을 완료하기 위해 당신의 메일 수신함(그리고 스팸 수신함)을 확인하세요.", + "ENTER_OTT": "인증 코드", + "RESEND_MAIL": "코드 재전송하기", + "VERIFY": "인증", + "UNKNOWN_ERROR": "문제가 생긴 것 같아요. 다시 시도하세요", + "INVALID_CODE": "잘못된 인증 코드", + "EXPIRED_CODE": "입력한 인증 코드가 만료되었습니다", + "SENDING": "전송 중...", + "SENT": "발송 완료!", + "PASSWORD": "비밀번호", + "LINK_PASSWORD": "앨범 잠금해제를 위해 비밀번호를 입력하세요", + "RETURN_PASSPHRASE_HINT": "비밀번호", + "SET_PASSPHRASE": "비밀번호 설정", + "VERIFY_PASSPHRASE": "로그인", + "INCORRECT_PASSPHRASE": "잘못된 비밀번호입니다", + "ENTER_ENC_PASSPHRASE": "당신의 데이터를 암호화하는 데 사용할 수 있는 비밀번호를 입력하세요", + "PASSPHRASE_DISCLAIMER": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키 없다면 <strong>데이터 복구를 도와드릴 수 없습니다</strong>.", + "WELCOME_TO_ENTE_HEADING": "환영합니다 <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "End-to-End 암호화된 사진 저장 및 공유", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "당신 최고의 사진이 있는 곳", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "암호 키 생성 중...", + "PASSPHRASE_HINT": "비밀번호", + "CONFIRM_PASSPHRASE": "비밀번호 확인", + "REFERRAL_CODE_HINT": "어떻게 Ente에 대해 들으셨나요? (선택사항)", + "REFERRAL_INFO": "우리는 앱 설치를 추적하지 않습니다. 우리를 알게 된 곳을 남겨주시면 우리에게 도움이 될꺼에요!", + "PASSPHRASE_MATCH_ERROR": "비밀번호가 일치하지 않습니다", + "CREATE_COLLECTION": "새 앨범", + "ENTER_ALBUM_NAME": "앨범 이름", + "CLOSE_OPTION": "닫기 (Esc)", + "ENTER_FILE_NAME": "파일 이름", + "CLOSE": "닫기", + "NO": "아니오", + "NOTHING_HERE": "아직 볼 수 있는 것이 없어요 👀", + "UPLOAD": "업로드", + "IMPORT": "가져오기", + "ADD_PHOTOS": "사진 추가", + "ADD_MORE_PHOTOS": "사진 더 추가하기", + "add_photos_one": "아이템 하나 추가", + "add_photos_other": "아이템 {{count, number}} 개 추가하기", + "SELECT_PHOTOS": "사진 선택하기", + "FILE_UPLOAD": "파일 업로드", + "UPLOAD_STAGE_MESSAGE": { + "0": "업로드 준비중", + "1": "구글 메타데이타 파일들 읽는중", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일 메타데이터가 추출되었습니다", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일이 처리되었습니다", + "4": "남은 업로드 취소중", + "5": "백업 완료" + }, + "FILE_NOT_UPLOADED_LIST": "아래 파일들은 업로드 되지 않았습니다", + "SUBSCRIPTION_EXPIRED": "구독 만료", + "SUBSCRIPTION_EXPIRED_MESSAGE": "당신 구독이 만료되었으니, 구독을 <a>갱신</a>해주세요", + "STORAGE_QUOTA_EXCEEDED": "스토리지 제한이 초과되었습니다", + "INITIAL_LOAD_DELAY_WARNING": "처음 로딩시 다소 시간이 걸릴 수 있습니다", + "USER_DOES_NOT_EXIST": "죄송합니다. 해당 이메일을 사용하는 사용자를 찾을 수 없습니다", + "NO_ACCOUNT": "계정이 없습니다", + "ACCOUNT_EXISTS": "이미 계정이 있습니다", + "CREATE": "만들기", + "DOWNLOAD": "다운로드", + "DOWNLOAD_OPTION": "다운로드 (D)", + "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/nl-NL/translation.json b/web/apps/cast/public/locales/nl-NL/translation.json new file mode 100644 index 000000000..15d9bfdba --- /dev/null +++ b/web/apps/cast/public/locales/nl-NL/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Privé back-ups</div><div>voor uw herinneringen</div>", + "HERO_SLIDE_1": "Standaard end-to-end versleuteld", + "HERO_SLIDE_2_TITLE": "<div>Veilig opgeslagen</div><div>in een kernbunker</div>", + "HERO_SLIDE_2": "Ontworpen om levenslang mee te gaan", + "HERO_SLIDE_3_TITLE": "<div>Overal</div><div> beschikbaar</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Desktop", + "LOGIN": "Inloggen", + "SIGN_UP": "Registreren", + "NEW_USER": "Nieuw bij ente", + "EXISTING_USER": "Bestaande gebruiker", + "ENTER_NAME": "Naam invoeren", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Voeg een naam toe zodat je vrienden weten wie ze moeten bedanken voor deze geweldige foto's!", + "ENTER_EMAIL": "Vul e-mailadres in", + "EMAIL_ERROR": "Vul een geldig e-mailadres in", + "REQUIRED": "Vereist", + "EMAIL_SENT": "Verificatiecode verzonden naar <a>{{email}}</a>", + "CHECK_INBOX": "Controleer je inbox (en spam) om verificatie te voltooien", + "ENTER_OTT": "Verificatiecode", + "RESEND_MAIL": "Code opnieuw versturen", + "VERIFY": "Verifiëren", + "UNKNOWN_ERROR": "Er is iets fout gegaan, probeer het opnieuw", + "INVALID_CODE": "Ongeldige verificatiecode", + "EXPIRED_CODE": "Uw verificatiecode is verlopen", + "SENDING": "Verzenden...", + "SENT": "Verzonden!", + "PASSWORD": "Wachtwoord", + "LINK_PASSWORD": "Voer wachtwoord in om het album te ontgrendelen", + "RETURN_PASSPHRASE_HINT": "Wachtwoord", + "SET_PASSPHRASE": "Wachtwoord instellen", + "VERIFY_PASSPHRASE": "Aanmelden", + "INCORRECT_PASSPHRASE": "Onjuist wachtwoord", + "ENTER_ENC_PASSPHRASE": "Voer een wachtwoord in dat we kunnen gebruiken om je gegevens te versleutelen", + "PASSPHRASE_DISCLAIMER": "We slaan je wachtwoord niet op, dus als je het vergeet, <strong>zullen we u niet kunnen helpen </strong>uw data te herstellen zonder een herstelcode.", + "WELCOME_TO_ENTE_HEADING": "Welkom bij <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Foto opslag en delen met end to end encryptie", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Waar je beste foto's leven", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Encryptiecodes worden gegenereerd...", + "PASSPHRASE_HINT": "Wachtwoord", + "CONFIRM_PASSPHRASE": "Wachtwoord bevestigen", + "REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)", + "REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", + "PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen", + "CREATE_COLLECTION": "Nieuw album", + "ENTER_ALBUM_NAME": "Album naam", + "CLOSE_OPTION": "Sluiten (Esc)", + "ENTER_FILE_NAME": "Bestandsnaam", + "CLOSE": "Sluiten", + "NO": "Nee", + "NOTHING_HERE": "Nog niets te zien hier 👀", + "UPLOAD": "Uploaden", + "IMPORT": "Importeren", + "ADD_PHOTOS": "Foto's toevoegen", + "ADD_MORE_PHOTOS": "Meer foto's toevoegen", + "add_photos_one": "1 foto toevoegen", + "add_photos_other": "{{count, number}} foto's toevoegen", + "SELECT_PHOTOS": "Selecteer foto's", + "FILE_UPLOAD": "Bestand uploaden", + "UPLOAD_STAGE_MESSAGE": { + "0": "Upload wordt voorbereid", + "1": "Lezen van Google metadata bestanden", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} bestanden metadata uitgepakt", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} bestanden geback-upt", + "4": "Resterende uploads worden geannuleerd", + "5": "Back-up voltooid" + }, + "FILE_NOT_UPLOADED_LIST": "De volgende bestanden zijn niet geüpload", + "SUBSCRIPTION_EXPIRED": "Abonnement verlopen", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Uw abonnement is verlopen, gelieve <a>vernieuwen</a>", + "STORAGE_QUOTA_EXCEEDED": "Opslaglimiet overschreden", + "INITIAL_LOAD_DELAY_WARNING": "Eerste keer laden kan enige tijd duren", + "USER_DOES_NOT_EXIST": "Sorry, we konden geen account met dat e-mailadres vinden", + "NO_ACCOUNT": "Heb nog geen account", + "ACCOUNT_EXISTS": "Heb al een account", + "CREATE": "Creëren", + "DOWNLOAD": "Downloaden", + "DOWNLOAD_OPTION": "Downloaden (D)", + "DOWNLOAD_FAVORITES": "Favorieten downloaden", + "DOWNLOAD_UNCATEGORIZED": "Ongecategoriseerd downloaden", + "DOWNLOAD_HIDDEN_ITEMS": "Verborgen bestanden downloaden", + "COPY_OPTION": "Kopiëren als PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Schakelen volledig scherm modus (F)", + "ZOOM_IN_OUT": "In/uitzoomen", + "PREVIOUS": "Vorige (←)", + "NEXT": "Volgende (→)", + "TITLE_PHOTOS": "Ente Foto's", + "TITLE_ALBUMS": "Ente Foto's", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "Je eerste foto uploaden", + "IMPORT_YOUR_FOLDERS": "Importeer uw mappen", + "UPLOAD_DROPZONE_MESSAGE": "Sleep om een back-up van je bestanden te maken", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Sleep om map aan watched folders toe te voegen", + "TRASH_FILES_TITLE": "Bestanden verwijderen?", + "TRASH_FILE_TITLE": "Verwijder bestand?", + "DELETE_FILES_TITLE": "Onmiddellijk verwijderen?", + "DELETE_FILES_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van je ente account.", + "DELETE": "Verwijderen", + "DELETE_OPTION": "Verwijderen (DEL)", + "FAVORITE_OPTION": "Favoriet (L)", + "UNFAVORITE_OPTION": "Verwijderen uit Favorieten (L)", + "MULTI_FOLDER_UPLOAD": "Meerdere mappen gedetecteerd", + "UPLOAD_STRATEGY_CHOICE": "Wilt u deze uploaden naar", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Één enkel album", + "OR": "of", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Aparte albums maken", + "SESSION_EXPIRED_MESSAGE": "Uw sessie is verlopen. Meld u opnieuw aan om verder te gaan", + "SESSION_EXPIRED": "Sessie verlopen", + "PASSWORD_GENERATION_FAILED": "Uw browser kon geen sterke sleutel genereren die voldoet aan onze versleutelingsstandaarden. Probeer de mobiele app of een andere browser te gebruiken", + "CHANGE_PASSWORD": "Wachtwoord wijzigen", + "GO_BACK": "Ga terug", + "RECOVERY_KEY": "Herstelsleutel", + "SAVE_LATER": "Doe dit later", + "SAVE": "Sleutel opslaan", + "RECOVERY_KEY_DESCRIPTION": "Als je je wachtwoord vergeet, kun je alleen met deze sleutel je gegevens herstellen.", + "RECOVER_KEY_GENERATION_FAILED": "Herstelcode kon niet worden gegenereerd, probeer het opnieuw", + "KEY_NOT_STORED_DISCLAIMER": "We slaan deze sleutel niet op, bewaar dit op een veilige plaats", + "FORGOT_PASSWORD": "Wachtwoord vergeten", + "RECOVER_ACCOUNT": "Account herstellen", + "RECOVERY_KEY_HINT": "Herstelsleutel", + "RECOVER": "Herstellen", + "NO_RECOVERY_KEY": "Geen herstelsleutel?", + "INCORRECT_RECOVERY_KEY": "Onjuiste herstelsleutel", + "SORRY": "Sorry", + "NO_RECOVERY_KEY_MESSAGE": "Door de aard van ons end-to-end encryptieprotocol kunnen je gegevens niet worden ontsleuteld zonder je wachtwoord of herstelsleutel", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Stuur een e-mail naar <a>{{emailID}}</a> vanaf het door jou geregistreerde e-mailadres", + "CONTACT_SUPPORT": "Klantenservice", + "REQUEST_FEATURE": "Vraag nieuwe functie aan", + "SUPPORT": "Ondersteuning", + "CONFIRM": "Bevestigen", + "CANCEL": "Annuleren", + "LOGOUT": "Uitloggen", + "DELETE_ACCOUNT": "Account verwijderen", + "DELETE_ACCOUNT_MESSAGE": "<p>Stuur een e-mail naar <a>{{emailID}}</a> vanaf uw geregistreerde e-mailadres.</p><p>Uw aanvraag wordt binnen 72 uur verwerkt.</p>", + "LOGOUT_MESSAGE": "Weet u zeker dat u wilt uitloggen?", + "CHANGE_EMAIL": "E-mail wijzigen", + "OK": "Oké", + "SUCCESS": "Succes", + "ERROR": "Foutmelding", + "MESSAGE": "Melding", + "INSTALL_MOBILE_APP": "Installeer onze <a>Android</a> of <b>iOS</b> app om automatisch een back-up te maken van al uw foto's", + "DOWNLOAD_APP_MESSAGE": "Sorry, deze bewerking wordt momenteel alleen ondersteund op onze desktop app", + "DOWNLOAD_APP": "Download de desktop app", + "EXPORT": "Data exporteren", + "SUBSCRIPTION": "Abonnement", + "SUBSCRIBE": "Abonneren", + "MANAGEMENT_PORTAL": "Betaalmethode beheren", + "MANAGE_FAMILY_PORTAL": "Familie abonnement beheren", + "LEAVE_FAMILY_PLAN": "Familie abonnement verlaten", + "LEAVE": "Verlaten", + "LEAVE_FAMILY_CONFIRM": "Weet je zeker dat je het familie-plan wilt verlaten?", + "CHOOSE_PLAN": "Kies uw abonnement", + "MANAGE_PLAN": "Beheer uw abonnement", + "ACTIVE": "Actief", + "OFFLINE_MSG": "Je bent offline, lokaal opgeslagen herinneringen worden getoond", + "FREE_SUBSCRIPTION_INFO": "Je hebt het <strong>gratis</strong> abonnement dat verloopt op {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "U hebt een familieplan dat beheerd wordt door", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Vernieuwt op {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Eindigt op {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Uw abonnement loopt af op {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Jouw {{storage, string}} add-on is geldig tot {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "U heeft uw opslaglimiet overschreden, gelieve <a>upgraden</a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>We hebben uw betaling ontvangen</p><p>Uw abonnement is geldig tot <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Uw aankoop is geannuleerd, probeer het opnieuw als u zich wilt abonneren", + "SUBSCRIPTION_PURCHASE_FAILED": "Betaling van abonnement mislukt Probeer het opnieuw", + "SUBSCRIPTION_UPDATE_FAILED": "Niet gelukt om abonnement bij te werken, probeer het opnieuw", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Het spijt ons, maar de betaling is mislukt bij het in rekening brengen van uw kaart, gelieve uw betaalmethode bij te werken en het opnieuw te proberen", + "STRIPE_AUTHENTICATION_FAILED": "We zijn niet in staat om uw betaalmethode te verifiëren. Kies een andere betaalmethode en probeer het opnieuw", + "UPDATE_PAYMENT_METHOD": "Betalingsmethode bijwerken", + "MONTHLY": "Maandelijks", + "YEARLY": "Jaarlijks", + "UPDATE_SUBSCRIPTION_MESSAGE": "Weet u zeker dat u uw abonnement wilt wijzigen?", + "UPDATE_SUBSCRIPTION": "Abonnement wijzigen", + "CANCEL_SUBSCRIPTION": "Abonnement opzeggen", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Al je gegevens zullen worden verwijderd van onze servers aan het einde van deze factureringsperiode.</p><p>Weet u zeker dat u uw abonnement wilt opzeggen?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Weet je zeker dat je je abonnement wilt opzeggen?</p>", + "SUBSCRIPTION_CANCEL_FAILED": "Abonnement opzeggen mislukt", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement succesvol geannuleerd", + "REACTIVATE_SUBSCRIPTION": "Abonnement opnieuw activeren", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Zodra je weer bent geactiveerd, zal je worden gefactureerd op {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Abonnement succesvol geactiveerd ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Heractiveren van abonnementsverlenging is mislukt", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Bedankt", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Mobiel abonnement opzeggen", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Annuleer je abonnement via de mobiele app om je abonnement hier te activeren", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Neem contact met ons op via <a>{{emailID}}</a> om uw abonnement te beheren", + "RENAME": "Naam wijzigen", + "RENAME_FILE": "Bestandsnaam wijzigen", + "RENAME_COLLECTION": "Albumnaam wijzigen", + "DELETE_COLLECTION_TITLE": "Verwijder album?", + "DELETE_COLLECTION": "Verwijder album", + "DELETE_COLLECTION_MESSAGE": "Verwijder de foto's (en video's) van dit album ook uit <a>alle</a> andere albums waar deze deel van uitmaken?", + "DELETE_PHOTOS": "Foto's verwijderen", + "KEEP_PHOTOS": "Foto's behouden", + "SHARE": "Delen", + "SHARE_COLLECTION": "Album delen", + "SHAREES": "Gedeeld met", + "SHARE_WITH_SELF": "Oeps, je kunt niet met jezelf delen", + "ALREADY_SHARED": "Oeps, je deelt dit al met {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Album delen niet toegestaan", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Delen is uitgeschakeld voor gratis accounts", + "DOWNLOAD_COLLECTION": "Download album", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Weet je zeker dat je het volledige album wilt downloaden?</p><p>Alle bestanden worden in de wachtrij geplaatst voor downloaden</p>", + "CREATE_ALBUM_FAILED": "Aanmaken van album mislukt, probeer het opnieuw", + "SEARCH": "Zoeken", + "SEARCH_RESULTS": "Zoekresultaten", + "NO_RESULTS": "Geen resultaten gevonden", + "SEARCH_HINT": "Zoeken naar albums, datums ...", + "SEARCH_TYPE": { + "COLLECTION": "Album", + "LOCATION": "Locatie", + "CITY": "Locatie", + "DATE": "Datum", + "FILE_NAME": "Bestandsnaam", + "THING": "Inhoud", + "FILE_CAPTION": "Omschrijving", + "FILE_TYPE": "Bestandstype", + "CLIP": "Magische" + }, + "photos_count_zero": "Geen herinneringen", + "photos_count_one": "1 herinnering", + "photos_count_other": "{{count, number}} herinneringen", + "TERMS_AND_CONDITIONS": "Ik ga akkoord met de <a>gebruiksvoorwaarden</a> en <b>privacybeleid</b>", + "ADD_TO_COLLECTION": "Toevoegen aan album", + "SELECTED": "geselecteerd", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Deze video kan niet afgespeeld worden op uw browser", + "PEOPLE": "Personen", + "INDEXING_SCHEDULED": "indexering is gepland...", + "ANALYZING_PHOTOS": "analyseren van nieuwe foto's {{indexStatus.nSyncedFiles}} van {{indexStatus.nTotalFiles}} gedaan)...", + "INDEXING_PEOPLE": "mensen indexeren in {{indexStatus.nSyncedFiles}} foto's...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles}} geïndexeerde foto's", + "UNIDENTIFIED_FACES": "ongeïdentificeerde gezichten", + "OBJECTS": "objecten", + "TEXT": "tekst", + "INFO": "Info ", + "INFO_OPTION": "Info (I)", + "FILE_NAME": "Bestandsnaam", + "CAPTION_PLACEHOLDER": "Voeg een beschrijving toe", + "LOCATION": "Locatie", + "SHOW_ON_MAP": "Bekijk op OpenStreetMap", + "MAP": "Kaart", + "MAP_SETTINGS": "Kaart instellingen", + "ENABLE_MAPS": "Kaarten inschakelen?", + "ENABLE_MAP": "Kaarten inschakelen", + "DISABLE_MAPS": "Kaarten uitzetten?", + "ENABLE_MAP_DESCRIPTION": "<p> Dit toont jouw foto's op een wereldkaart.</p> <p> Deze kaart wordt gehost door <a>Open Street Map</a>, en de exacte locaties van jouw foto's worden nooit gedeeld.</p> <p> Je kunt deze functie op elk gewenst moment uitschakelen via de instellingen.</p>", + "DISABLE_MAP_DESCRIPTION": "<p>Dit schakelt de weergave van je foto's op een wereldkaart uit.</p> <p>Je kunt deze functie op elk gewenst moment inschakelen via Instellingen.</p>", + "DISABLE_MAP": "Kaarten uitzetten", + "DETAILS": "Details", + "VIEW_EXIF": "Bekijk alle EXIF gegevens", + "NO_EXIF": "Geen EXIF gegevens", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Tweestaps", + "TWO_FACTOR_AUTHENTICATION": "Tweestapsverificatie", + "TWO_FACTOR_QR_INSTRUCTION": "Scan de onderstaande QR-code met uw favoriete verificatie app", + "ENTER_CODE_MANUALLY": "Voer de code handmatig in", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Voer deze code in in uw favoriete verificatie app", + "SCAN_QR_CODE": "Scan QR-code in plaats daarvan", + "ENABLE_TWO_FACTOR": "Tweestapsverificatie inschakelen", + "ENABLE": "Inschakelen", + "LOST_DEVICE": "Tweestapsverificatie apparaat verloren", + "INCORRECT_CODE": "Onjuiste code", + "TWO_FACTOR_INFO": "Voeg een extra beveiligingslaag toe door meer dan uw e-mailadres en wachtwoord te vereisen om in te loggen op uw account", + "DISABLE_TWO_FACTOR_LABEL": "Schakel tweestapsverificatie uit", + "UPDATE_TWO_FACTOR_LABEL": "Update uw verificatie apparaat", + "DISABLE": "Uitschakelen", + "RECONFIGURE": "Herconfigureren", + "UPDATE_TWO_FACTOR": "Tweestapsverificatie bijwerken", + "UPDATE_TWO_FACTOR_MESSAGE": "Verder gaan zal elk eerder geconfigureerde verificatie apparaat ontzeggen", + "UPDATE": "Bijwerken", + "DISABLE_TWO_FACTOR": "Tweestapsverificatie uitschakelen", + "DISABLE_TWO_FACTOR_MESSAGE": "Weet u zeker dat u tweestapsverificatie wilt uitschakelen", + "TWO_FACTOR_DISABLE_FAILED": "Uitschakelen van tweestapsverificatie is mislukt, probeer het opnieuw", + "EXPORT_DATA": "Gegevens exporteren", + "SELECT_FOLDER": "Map selecteren", + "DESTINATION": "Bestemming", + "START": "Start", + "LAST_EXPORT_TIME": "Tijd laatste export", + "EXPORT_AGAIN": "Opnieuw synchroniseren", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokale opslag niet toegankelijk", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Je browser of een extensie blokkeert ente om gegevens op te slaan in de lokale opslag. Probeer deze pagina te laden na het aanpassen van de browser surfmodus.", + "SEND_OTT": "Stuur OTP", + "EMAIl_ALREADY_OWNED": "E-mail al in gebruik", + "ETAGS_BLOCKED": "<p>We kunnen de volgende bestanden niet uploaden vanwege uw browserconfiguratie.</p><p>Schakel alle extensies uit die mogelijk voorkomen dat ente <code>eTags</code> kan gebruiken om grote bestanden te uploaden, of gebruik onze <a>desktop app</a> voor een betrouwbaardere import ervaring.</p>", + "SKIPPED_VIDEOS_INFO": "<p>We ondersteunen het toevoegen van video's via openbare links momenteel niet.</p><p>Om video's te delen, <a>meld je aan</a> bij ente en deel met de beoogde ontvangers via hun e-mail</p>", + "LIVE_PHOTOS_DETECTED": "De foto en video bestanden van je Live Photos zijn samengevoegd tot één enkel bestand", + "RETRY_FAILED": "Probeer mislukte uploads nogmaals", + "FAILED_UPLOADS": "Mislukte uploads ", + "SKIPPED_FILES": "Genegeerde uploads", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generatie mislukt", + "UNSUPPORTED_FILES": "Niet-ondersteunde bestanden", + "SUCCESSFUL_UPLOADS": "Succesvolle uploads", + "SKIPPED_INFO": "Deze zijn overgeslagen omdat er bestanden zijn met overeenkomende namen in hetzelfde album", + "UNSUPPORTED_INFO": "ente ondersteunt deze bestandsformaten nog niet", + "BLOCKED_UPLOADS": "Geblokkeerde uploads", + "SKIPPED_VIDEOS": "Overgeslagen video's", + "INPROGRESS_METADATA_EXTRACTION": "In behandeling", + "INPROGRESS_UPLOADS": "Bezig met uploaden", + "TOO_LARGE_UPLOADS": "Grote bestanden", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Onvoldoende opslagruimte", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Deze bestanden zijn niet geüpload omdat ze de maximale grootte van uw opslagplan overschrijden", + "TOO_LARGE_INFO": "Deze bestanden zijn niet geüpload omdat ze onze limiet voor bestandsgrootte overschrijden", + "THUMBNAIL_GENERATION_FAILED_INFO": "Deze bestanden zijn geüpload, maar helaas konden we geen thumbnails voor ze genereren.", + "UPLOAD_TO_COLLECTION": "Uploaden naar album", + "UNCATEGORIZED": "Ongecategoriseerd", + "ARCHIVE": "Archiveren", + "FAVORITES": "Favorieten", + "ARCHIVE_COLLECTION": "Album archiveren", + "ARCHIVE_SECTION_NAME": "Archief", + "ALL_SECTION_NAME": "Alle", + "MOVE_TO_COLLECTION": "Verplaats naar album", + "UNARCHIVE": "Uit archief halen", + "UNARCHIVE_COLLECTION": "Album uit archief halen", + "HIDE_COLLECTION": "Verberg album", + "UNHIDE_COLLECTION": "Album zichtbaar maken", + "MOVE": "Verplaatsen", + "ADD": "Toevoegen", + "REMOVE": "Verwijderen", + "YES_REMOVE": "Ja, verwijderen", + "REMOVE_FROM_COLLECTION": "Verwijderen uit album", + "TRASH": "Prullenbak", + "MOVE_TO_TRASH": "Verplaatsen naar prullenbak", + "TRASH_FILES_MESSAGE": "De geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak.", + "TRASH_FILE_MESSAGE": "Het bestand wordt uit alle albums verwijderd en verplaatst naar de prullenbak.", + "DELETE_PERMANENTLY": "Permanent verwijderen", + "RESTORE": "Herstellen", + "RESTORE_TO_COLLECTION": "Terugzetten naar album", + "EMPTY_TRASH": "Prullenbak leegmaken", + "EMPTY_TRASH_TITLE": "Prullenbak leegmaken?", + "EMPTY_TRASH_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van uw ente account.", + "LEAVE_SHARED_ALBUM": "Ja, verwijderen", + "LEAVE_ALBUM": "Album verlaten", + "LEAVE_SHARED_ALBUM_TITLE": "Gedeeld album verwijderen?", + "LEAVE_SHARED_ALBUM_MESSAGE": "Je verlaat het album, en het zal niet meer zichtbaar voor je zijn.", + "NOT_FILE_OWNER": "U kunt bestanden niet verwijderen in een gedeeld album", + "CONFIRM_SELF_REMOVE_MESSAGE": "De geselecteerde items worden verwijderd uit dit album. De items die alleen in dit album staan, worden verplaatst naar 'Niet gecategoriseerd'.", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Sommige van de items die u verwijdert zijn door andere mensen toegevoegd, en u verliest de toegang daartoe.", + "SORT_BY_CREATION_TIME_ASCENDING": "Oudste", + "SORT_BY_UPDATION_TIME_DESCENDING": "Laatst gewijzigd op", + "SORT_BY_NAME": "Naam", + "COMPRESS_THUMBNAILS": "Comprimeren van thumbnails", + "THUMBNAIL_REPLACED": "Thumbnails gecomprimeerd", + "FIX_THUMBNAIL": "Comprimeren", + "FIX_THUMBNAIL_LATER": "Later comprimeren", + "REPLACE_THUMBNAIL_NOT_STARTED": "Sommige van uw video thumbnails kunnen worden gecomprimeerd om ruimte te besparen. Wilt u dat ente ze comprimeert?", + "REPLACE_THUMBNAIL_COMPLETED": "Alle thumbnails zijn gecomprimeerd", + "REPLACE_THUMBNAIL_NOOP": "Je hebt geen thumbnails die verder gecomprimeerd kunnen worden", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Kon sommige van uw thumbnails niet comprimeren, probeer het opnieuw", + "FIX_CREATION_TIME": "Herstel tijd", + "FIX_CREATION_TIME_IN_PROGRESS": "Tijd aan het herstellen", + "CREATION_TIME_UPDATED": "Bestandstijd bijgewerkt", + "UPDATE_CREATION_TIME_NOT_STARTED": "Selecteer de optie die u wilt gebruiken", + "UPDATE_CREATION_TIME_COMPLETED": "Alle bestanden succesvol bijgewerkt", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Bestandstijd update mislukt voor sommige bestanden, probeer het opnieuw", + "CAPTION_CHARACTER_LIMIT": "5000 tekens max", + "DATE_TIME_ORIGINAL": "EXIF:DatumTijdOrigineel", + "DATE_TIME_DIGITIZED": "EXIF:DatumTijdDigitaliseerd", + "METADATA_DATE": "EXIF:MetadataDatum", + "CUSTOM_TIME": "Aangepaste tijd", + "REOPEN_PLAN_SELECTOR_MODAL": "Abonnementen heropenen", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Kon abonnementen niet openen", + "INSTALL": "Installeren", + "SHARING_DETAILS": "Delen van informatie", + "MODIFY_SHARING": "Delen wijzigen", + "ADD_COLLABORATORS": "Samenwerker toevoegen", + "ADD_NEW_EMAIL": "Nieuw e-mailadres toevoegen", + "shared_with_people_zero": "Delen met specifieke mensen", + "shared_with_people_one": "Gedeeld met 1 persoon", + "shared_with_people_other": "Gedeeld met {{count, number}} mensen", + "participants_zero": "Geen deelnemers", + "participants_one": "1 deelnemer", + "participants_other": "{{count, number}} deelnemers", + "ADD_VIEWERS": "Voeg kijkers toe", + "PARTICIPANTS": "Deelnemers", + "CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} zullen geen foto's meer kunnen toevoegen aan dit album</p> <p> Ze zullen nog steeds bestaande foto's kunnen verwijderen die door hen zijn toegevoegd</p>", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} zal foto's aan het album kunnen toevoegen", + "CONVERT_TO_VIEWER": "Ja, converteren naar kijker", + "CONVERT_TO_COLLABORATOR": "Ja, converteren naar samenwerker", + "CHANGE_PERMISSION": "Rechten aanpassen?", + "REMOVE_PARTICIPANT": "Verwijderen?", + "CONFIRM_REMOVE": "Ja, verwijderen", + "MANAGE": "Beheren", + "ADDED_AS": "Toegevoegd als", + "COLLABORATOR_RIGHTS": "Samenwerkers kunnen foto's en video's toevoegen aan het gedeelde album", + "REMOVE_PARTICIPANT_HEAD": "Deelnemer verwijderen", + "OWNER": "Eigenaar", + "COLLABORATORS": "Samenwerker", + "ADD_MORE": "Meer toevoegen", + "VIEWERS": "Kijkers", + "OR_ADD_EXISTING": "Of kies een bestaande", + "REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} zullen worden verwijderd uit het gedeelde album</p> <p>Alle door hen toegevoegde foto's worden ook uit het album verwijderd</p>", + "NOT_FOUND": "404 - niet gevonden", + "LINK_EXPIRED": "Link verlopen", + "LINK_EXPIRED_MESSAGE": "Deze link is verlopen of uitgeschakeld!", + "MANAGE_LINK": "Link beheren", + "LINK_TOO_MANY_REQUESTS": "Dit album is te populair voor ons om te verwerken!", + "FILE_DOWNLOAD": "Downloads toestaan", + "LINK_PASSWORD_LOCK": "Wachtwoord versleuteling", + "PUBLIC_COLLECT": "Foto's toevoegen toestaan", + "LINK_DEVICE_LIMIT": "Apparaat limiet", + "NO_DEVICE_LIMIT": "Geen", + "LINK_EXPIRY": "Vervaldatum link", + "NEVER": "Nooit", + "DISABLE_FILE_DOWNLOAD": "Download uitschakelen", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>Weet u zeker dat u de downloadknop voor bestanden wilt uitschakelen?</p><p>Kijkers kunnen nog steeds screenshots maken of een kopie van uw foto's opslaan met behulp van externe hulpmiddelen.</p>", + "MALICIOUS_CONTENT": "Bevat kwaadwillende inhoud", + "COPYRIGHT": "Schending van het auteursrecht van iemand die ik mag vertegenwoordigen", + "SHARED_USING": "Gedeeld via ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "Gebruik code <strong>{{referralCode}}</strong> om 10 GB gratis te krijgen", + "LIVE": "LIVE", + "DISABLE_PASSWORD": "Schakel cijfercode vergrendeling uit", + "DISABLE_PASSWORD_MESSAGE": "Weet u zeker dat u de cijfercode vergrendeling wilt uitschakelen?", + "PASSWORD_LOCK": "Cijfercode vergrendeling", + "LOCK": "Vergrendeling", + "DOWNLOAD_UPLOAD_LOGS": "Logboeken voor foutmeldingen", + "UPLOAD_FILES": "Bestand", + "UPLOAD_DIRS": "Map", + "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", + "DEDUPLICATE_FILES": "Dubbele bestanden verwijderen", + "AUTHENTICATOR_SECTION": "Verificatie apparaat", + "NO_DUPLICATES_FOUND": "Je hebt geen dubbele bestanden die kunnen worden gewist", + "CLUB_BY_CAPTURE_TIME": "Samenvoegen op tijd", + "FILES": "Bestanden", + "EACH": "Elke", + "DEDUPLICATE_BASED_ON_SIZE": "De volgende bestanden zijn samengevoegd op basis van hun groottes. Controleer en verwijder items waarvan je denkt dat ze dubbel zijn", + "STOP_ALL_UPLOADS_MESSAGE": "Weet u zeker dat u wilt stoppen met alle uploads die worden uitgevoerd?", + "STOP_UPLOADS_HEADER": "Stoppen met uploaden?", + "YES_STOP_UPLOADS": "Ja, stop uploaden", + "STOP_DOWNLOADS_HEADER": "Downloaden stoppen?", + "YES_STOP_DOWNLOADS": "Ja, downloads stoppen", + "STOP_ALL_DOWNLOADS_MESSAGE": "Weet je zeker dat je wilt stoppen met alle downloads die worden uitgevoerd?", + "albums_one": "1 Album", + "albums_other": "{{count, number}} Albums", + "ALL_ALBUMS": "Alle albums", + "ALBUMS": "Albums", + "ALL_HIDDEN_ALBUMS": "Alle verborgen albums", + "HIDDEN_ALBUMS": "Verborgen albums", + "HIDDEN_ITEMS": "Verborgen bestanden", + "HIDDEN_ITEMS_SECTION_NAME": "Verborgen_items", + "ENTER_TWO_FACTOR_OTP": "Voer de 6-cijferige code van uw verificatie app in.", + "CREATE_ACCOUNT": "Account aanmaken", + "COPIED": "Gekopieerd", + "CANVAS_BLOCKED_TITLE": "Kan thumbnail niet genereren", + "CANVAS_BLOCKED_MESSAGE": "<p>Het lijkt erop dat uw browser geen toegang heeft tot canvas, die nodig is om thumbnails voor uw foto's te genereren </p> <p> Schakel toegang tot het canvas van uw browser in, of bekijk onze desktop app</p>", + "WATCH_FOLDERS": "Monitor mappen", + "UPGRADE_NOW": "Nu upgraden", + "RENEW_NOW": "Nu verlengen", + "STORAGE": "Opslagruimte", + "USED": "gebruikt", + "YOU": "Jij", + "FAMILY": "Familie", + "FREE": "free", + "OF": "van", + "WATCHED_FOLDERS": "Gemonitorde mappen", + "NO_FOLDERS_ADDED": "Nog geen mappen toegevoegd!", + "FOLDERS_AUTOMATICALLY_MONITORED": "De mappen die u hier toevoegt worden automatisch gemonitord", + "UPLOAD_NEW_FILES_TO_ENTE": "Nieuwe bestanden uploaden naar ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Verwijderde bestanden van ente opruimen", + "ADD_FOLDER": "Map toevoegen", + "STOP_WATCHING": "Stop monitoren", + "STOP_WATCHING_FOLDER": "Stop monitoren van map?", + "STOP_WATCHING_DIALOG_MESSAGE": "Uw bestaande bestanden zullen niet worden verwijderd, maar ente stopt met het automatisch bijwerken van het gekoppelde ente album bij wijzigingen in deze map.", + "YES_STOP": "Ja, stop", + "MONTH_SHORT": "mo", + "YEAR": "jaar", + "FAMILY_PLAN": "Familie abonnement", + "DOWNLOAD_LOGS": "Logboek downloaden", + "DOWNLOAD_LOGS_MESSAGE": "<p>Dit zal logboeken downloaden, die u ons kunt e-mailen om te helpen bij het debuggen van uw probleem.</p><p> Houd er rekening mee dat bestandsnamen worden opgenomen om problemen met specifieke bestanden bij te houden. </p>", + "CHANGE_FOLDER": "Map wijzigen", + "TWO_MONTHS_FREE": "Krijg 2 maanden gratis op jaarlijkse abonnementen", + "GB": "GB", + "POPULAR": "Populair", + "FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account", + "FREE_PLAN_DESCRIPTION": "1 GB voor 1 jaar", + "CURRENT_USAGE": "Huidig gebruik is <strong>{{usage}}</strong>", + "WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de ente mobiel/desktop app.", + "DRAG_AND_DROP_HINT": "Of sleep en plaats in het ente venster", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Uw geüploade gegevens worden gepland voor verwijdering, en uw account zal permanent worden verwijderd.<br/><br/>Deze actie is onomkeerbaar.", + "AUTHENTICATE": "Verifiëren", + "UPLOADED_TO_SINGLE_COLLECTION": "Geüpload naar enkele collectie", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "Geüpload naar verschillende collecties", + "NEVERMIND": "Laat maar", + "UPDATE_AVAILABLE": "Update beschikbaar", + "UPDATE_INSTALLABLE_MESSAGE": "Er staat een nieuwe versie van ente klaar om te worden geïnstalleerd.", + "INSTALL_NOW": "Nu installeren", + "INSTALL_ON_NEXT_LAUNCH": "Installeren bij volgende start", + "UPDATE_AVAILABLE_MESSAGE": "Er is een nieuwe versie van ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", + "DOWNLOAD_AND_INSTALL": "Downloaden en installeren", + "IGNORE_THIS_VERSION": "Negeer deze versie", + "TODAY": "Vandaag", + "YESTERDAY": "Gisteren", + "NAME_PLACEHOLDER": "Naam...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Kan geen albums maken uit bestand/map mix", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Je hebt een mix van bestanden en mappen gesleept en laten vallen.</p><p>Geef ofwel alleen bestanden aan, of alleen mappen bij het selecteren van de optie om afzonderlijke albums te maken</p>", + "CHOSE_THEME": "Kies thema", + "ML_SEARCH": "ML zoeken (bèta)", + "ENABLE_ML_SEARCH_DESCRIPTION": "<p>Dit zal algoritmes op het apparaat inschakelen die zullen beginnen met het lokaal analyseren van uw geüploade foto's.</p><p>Voor het eerst na inloggen of het inschakelen van deze functie zal het alle afbeeldingen op het lokale apparaat downloaden om ze te analyseren. Schakel dit dus alleen in als je akkoord bent met gegevensverbruik en lokale verwerking van alle afbeeldingen in uw fotobibliotheek.</p><p>Als dit de eerste keer is dat uw dit inschakelt, vragen we u ook om toestemming om gegevens te verwerken.</p>", + "ML_MORE_DETAILS": "Meer details", + "ENABLE_FACE_SEARCH": "Zoeken op gezichten inschakelen", + "ENABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten inschakelen?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "<p>Als u zoeken op gezichten inschakelt, analyseert ente de gezichtsgeometrie uit uw foto's. Dit gebeurt op uw apparaat en alle gegenereerde biometrische gegevens worden end-to-end versleuteld.<p/><p><a>Klik hier voor meer informatie over deze functie in ons privacybeleid</a></p>", + "DISABLE_BETA": "Bèta uitschakelen", + "DISABLE_FACE_SEARCH": "Zoeken op gezichten uitschakelen", + "DISABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten uitschakelen?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente zal stoppen met het analyseren van de gezichtsgeometrie, en zal ML zoeken (beta) uitschakelen</p><p>U kan zoeken op gezichten opnieuw inschakelen wanneer u wilt, dus deze handeling is veilig.</p>", + "ADVANCED": "Geavanceerd", + "FACE_SEARCH_CONFIRMATION": "Ik begrijp het, en wil ente toestaan om gezichten te analyseren", + "LABS": "Lab's", + "YOURS": "jouw", + "PASSPHRASE_STRENGTH_WEAK": "Wachtwoord sterkte: Zwak", + "PASSPHRASE_STRENGTH_MODERATE": "Wachtwoord sterkte: Matig", + "PASSPHRASE_STRENGTH_STRONG": "Wachtwoord sterkte: Sterk", + "PREFERENCES": "Instellingen", + "LANGUAGE": "Taal", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Ongeldige export map", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>De export map die u heeft geselecteerd bestaat niet.</p><p> Selecteer een geldige map.</p>", + "SUBSCRIPTION_VERIFICATION_ERROR": "Abonnementsverificatie mislukt", + "STORAGE_UNITS": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "na één uur", + "DAY": "na één dag", + "WEEK": "na één week", + "MONTH": "na één maand", + "YEAR": "na één jaar" + }, + "COPY_LINK": "Link kopiëren", + "DONE": "Voltooid", + "LINK_SHARE_TITLE": "Of deel een link", + "REMOVE_LINK": "Link verwijderen", + "CREATE_PUBLIC_SHARING": "Maak publieke link", + "PUBLIC_LINK_CREATED": "Publieke link aangemaakt", + "PUBLIC_LINK_ENABLED": "Publieke link ingeschakeld", + "COLLECT_PHOTOS": "Foto's verzamelen", + "PUBLIC_COLLECT_SUBTEXT": "Sta toe dat mensen met de link ook foto's kunnen toevoegen aan het gedeelde album.", + "STOP_EXPORT": "Stoppen", + "EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> bestanden geëxporteerd", + "MIGRATING_EXPORT": "Voorbereiden...", + "RENAMING_COLLECTION_FOLDERS": "Albumnamen hernoemen...", + "TRASHING_DELETED_FILES": "Verwijderde bestanden naar prullenbak...", + "TRASHING_DELETED_COLLECTIONS": "Verwijderde albums naar prullenbak...", + "EXPORT_NOTIFICATION": { + "START": "Exporteren begonnen", + "IN_PROGRESS": "Exporteren is al bezig", + "FINISH": "Exporteren voltooid", + "UP_TO_DATE": "Geen nieuwe bestanden om te exporteren" + }, + "CONTINUOUS_EXPORT": "Continue synchroniseren", + "TOTAL_ITEMS": "Totaal aantal bestanden", + "PENDING_ITEMS": "Bestanden in behandeling", + "EXPORT_STARTING": "Exporteren begonnen...", + "DELETE_ACCOUNT_REASON_LABEL": "Wat is de belangrijkste reden waarom je jouw account verwijdert?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Kies een reden", + "DELETE_REASON": { + "MISSING_FEATURE": "Ik mis een belangrijke functie", + "BROKEN_BEHAVIOR": "De app of een bepaalde functie functioneert niet zoals ik verwacht", + "FOUND_ANOTHER_SERVICE": "Ik heb een andere dienst gevonden die me beter bevalt", + "NOT_LISTED": "Mijn reden wordt niet vermeld" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "We vinden het jammer je te zien gaan. Deel alsjeblieft je feedback om ons te helpen verbeteren.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Ja, ik wil permanent mijn account inclusief alle gegevens verwijderen", + "CONFIRM_DELETE_ACCOUNT": "Account verwijderen bevestigen", + "FEEDBACK_REQUIRED": "Help ons alsjeblieft met deze informatie", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Wat doet de andere dienst beter?", + "RECOVER_TWO_FACTOR": "Herstel tweestaps", + "at": "om", + "AUTH_NEXT": "volgende", + "AUTH_DOWNLOAD_MOBILE_APP": "Download onze mobiele app om uw geheimen te beheren", + "HIDDEN": "Verborgen", + "HIDE": "Verbergen", + "UNHIDE": "Zichtbaar maken", + "UNHIDE_TO_COLLECTION": "Zichtbaar maken in album", + "SORT_BY": "Sorteren op", + "NEWEST_FIRST": "Nieuwste eerst", + "OLDEST_FIRST": "Oudste eerst", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Dit bestand kan niet worden bekeken in de app, klik hier om het origineel te downloaden", + "SELECT_COLLECTION": "Album selecteren", + "PIN_ALBUM": "Album bovenaan vastzetten", + "UNPIN_ALBUM": "Album losmaken", + "DOWNLOAD_COMPLETE": "Download compleet", + "DOWNLOADING_COLLECTION": "{{name}} downloaden", + "DOWNLOAD_FAILED": "Download mislukt", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} bestanden", + "CHRISTMAS": "Kerst", + "CHRISTMAS_EVE": "Kerstavond", + "NEW_YEAR": "Nieuwjaar", + "NEW_YEAR_EVE": "Oudjaarsavond", + "IMAGE": "Afbeelding", + "VIDEO": "Video", + "LIVE_PHOTO": "Live foto", + "CONVERT": "Converteren", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Weet u zeker dat u de editor wilt afsluiten?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download uw bewerkte afbeelding of sla een kopie op in ente om uw wijzigingen te behouden.", + "BRIGHTNESS": "Helderheid", + "CONTRAST": "Contrast", + "SATURATION": "Saturatie", + "BLUR": "Vervagen", + "INVERT_COLORS": "Kleuren omkeren", + "ASPECT_RATIO": "Beeldverhouding", + "SQUARE": "Vierkant", + "ROTATE_LEFT": "Roteer links", + "ROTATE_RIGHT": "Roteer rechts", + "FLIP_VERTICALLY": "Verticaal spiegelen", + "FLIP_HORIZONTALLY": "Horizontaal spiegelen", + "DOWNLOAD_EDITED": "Download Bewerkt", + "SAVE_A_COPY_TO_ENTE": "Kopie in ente opslaan", + "RESTORE_ORIGINAL": "Origineel herstellen", + "TRANSFORM": "Transformeer", + "COLORS": "Kleuren", + "FLIP": "Omdraaien", + "ROTATION": "Draaiing", + "RESET": "Herstellen", + "PHOTO_EDITOR": "Fotobewerker", + "FASTER_UPLOAD": "Snellere uploads", + "FASTER_UPLOAD_DESCRIPTION": "Uploaden door nabije servers", + "MAGIC_SEARCH_STATUS": "Magische Zoekfunctie Status", + "INDEXED_ITEMS": "Geïndexeerde bestanden", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "Cache map", + "FREEHAND": "Losse hand", + "APPLY_CROP": "Bijsnijden toepassen", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/pt-BR/translation.json b/web/apps/cast/public/locales/pt-BR/translation.json new file mode 100644 index 000000000..0da001742 --- /dev/null +++ b/web/apps/cast/public/locales/pt-BR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Backups privados</div><div>para as suas memórias</div>", + "HERO_SLIDE_1": "Criptografia de ponta a ponta por padrão", + "HERO_SLIDE_2_TITLE": "<div>Armazenado com segurança</div><div>em um abrigo avançado</div>", + "HERO_SLIDE_2": "Feito para ter logenvidade", + "HERO_SLIDE_3_TITLE": "<div>Disponível</div><div> em qualquer lugar</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Desktop", + "LOGIN": "Entrar", + "SIGN_UP": "Registrar", + "NEW_USER": "Novo no ente", + "EXISTING_USER": "Usuário existente", + "ENTER_NAME": "Insira o nome", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", + "ENTER_EMAIL": "Insira o endereço de e-mail", + "EMAIL_ERROR": "Inserir um endereço de e-mail válido", + "REQUIRED": "Obrigatório", + "EMAIL_SENT": "Código de verificação enviado para <a>{{email}}</a>", + "CHECK_INBOX": "Verifique a sua caixa de entrada (e spam) para concluir a verificação", + "ENTER_OTT": "Código de verificação", + "RESEND_MAIL": "Reenviar código", + "VERIFY": "Verificar", + "UNKNOWN_ERROR": "Ocorreu um erro. Tente novamente", + "INVALID_CODE": "Código de verificação inválido", + "EXPIRED_CODE": "O seu código de verificação expirou", + "SENDING": "Enviando...", + "SENT": "Enviado!", + "PASSWORD": "Senha", + "LINK_PASSWORD": "Insira a senha para desbloquear o álbum", + "RETURN_PASSPHRASE_HINT": "Senha", + "SET_PASSPHRASE": "Definir senha", + "VERIFY_PASSPHRASE": "Iniciar sessão", + "INCORRECT_PASSPHRASE": "Palavra-passe incorreta", + "ENTER_ENC_PASSPHRASE": "Por favor, digite uma senha que podemos usar para criptografar seus dados", + "PASSPHRASE_DISCLAIMER": "Não armazenamos sua senha, portanto, se você esquecê-la, <strong>não poderemos ajudar</strong>na recuperação de seus dados sem uma chave de recuperação.", + "WELCOME_TO_ENTE_HEADING": "Bem-vindo ao <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "Armazenamento criptografado de ponta a ponta de fotos e compartilhamento", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Onde suas melhores fotos vivem", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Gerando chaves de criptografia...", + "PASSPHRASE_HINT": "Senha", + "CONFIRM_PASSPHRASE": "Confirmar senha", + "REFERRAL_CODE_HINT": "Como você ouviu sobre o Ente? (opcional)", + "REFERRAL_INFO": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", + "PASSPHRASE_MATCH_ERROR": "As senhas não coincidem", + "CREATE_COLLECTION": "Novo álbum", + "ENTER_ALBUM_NAME": "Nome do álbum", + "CLOSE_OPTION": "Fechar (Esc)", + "ENTER_FILE_NAME": "Nome do arquivo", + "CLOSE": "Fechar", + "NO": "Não", + "NOTHING_HERE": "Nada para ver aqui! 👀", + "UPLOAD": "Enviar", + "IMPORT": "Importar", + "ADD_PHOTOS": "Adicionar fotos", + "ADD_MORE_PHOTOS": "Adicionar mais fotos", + "add_photos_one": "Adicionar item", + "add_photos_other": "Adicionar {{count, number}} itens", + "SELECT_PHOTOS": "Selecionar fotos", + "FILE_UPLOAD": "Envio de Arquivo", + "UPLOAD_STAGE_MESSAGE": { + "0": "Preparando para enviar", + "1": "Lendo arquivos de metadados do google", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} metadados dos arquivos extraídos", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} arquivos processados", + "4": "Cancelando envios restante", + "5": "Backup concluído" + }, + "FILE_NOT_UPLOADED_LIST": "Os seguintes arquivos não foram enviados", + "SUBSCRIPTION_EXPIRED": "Assinatura expirada", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Sua assinatura expirou, por favor <a>renove-a</a>", + "STORAGE_QUOTA_EXCEEDED": "Limite de armazenamento excedido", + "INITIAL_LOAD_DELAY_WARNING": "Primeiro carregamento pode levar algum tempo", + "USER_DOES_NOT_EXIST": "Desculpe, não foi possível encontrar um usuário com este e-mail", + "NO_ACCOUNT": "Não possui uma conta", + "ACCOUNT_EXISTS": "Já possui uma conta", + "CREATE": "Criar", + "DOWNLOAD": "Baixar", + "DOWNLOAD_OPTION": "Baixar (D)", + "DOWNLOAD_FAVORITES": "Baixar favoritos", + "DOWNLOAD_UNCATEGORIZED": "Baixar não categorizado", + "DOWNLOAD_HIDDEN_ITEMS": "Baixar itens ocultos", + "COPY_OPTION": "Copiar como PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Mudar para tela cheia (F)", + "ZOOM_IN_OUT": "Ampliar/Reduzir", + "PREVIOUS": "Anterior (←)", + "NEXT": "Próximo (→)", + "TITLE_PHOTOS": "Ente Fotos", + "TITLE_ALBUMS": "Ente Fotos", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "Envie sua primeira foto", + "IMPORT_YOUR_FOLDERS": "Importar suas pastas", + "UPLOAD_DROPZONE_MESSAGE": "Arraste para salvar seus arquivos", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Arraste para adicionar pasta monitorada", + "TRASH_FILES_TITLE": "Excluir arquivos?", + "TRASH_FILE_TITLE": "Excluir arquivo?", + "DELETE_FILES_TITLE": "Excluir imediatamente?", + "DELETE_FILES_MESSAGE": "Os arquivos selecionados serão excluídos permanentemente da sua conta ente.", + "DELETE": "Excluir", + "DELETE_OPTION": "Excluir (DEL)", + "FAVORITE_OPTION": "Favorito (L)", + "UNFAVORITE_OPTION": "Remover Favorito (L)", + "MULTI_FOLDER_UPLOAD": "Várias pastas detectadas", + "UPLOAD_STRATEGY_CHOICE": "Gostaria de enviá-los para", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Um único álbum", + "OR": "ou", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Álbuns separados", + "SESSION_EXPIRED_MESSAGE": "A sua sessão expirou. Por favor inicie sessão novamente para continuar", + "SESSION_EXPIRED": "Sessão expirada", + "PASSWORD_GENERATION_FAILED": "Seu navegador foi incapaz de gerar uma chave forte que atende aos padrões de criptografia, por favor, tente usar o aplicativo móvel ou outro navegador", + "CHANGE_PASSWORD": "Alterar senha", + "GO_BACK": "Voltar", + "RECOVERY_KEY": "Chave de recuperação", + "SAVE_LATER": "Fazer isso mais tarde", + "SAVE": "Salvar Chave", + "RECOVERY_KEY_DESCRIPTION": "Caso você esqueça sua senha, a única maneira de recuperar seus dados é com essa chave.", + "RECOVER_KEY_GENERATION_FAILED": "Não foi possível gerar o código de recuperação, tente novamente", + "KEY_NOT_STORED_DISCLAIMER": "Não armazenamos essa chave, por favor, salve essa chave de palavras em um lugar seguro", + "FORGOT_PASSWORD": "Esqueci a senha", + "RECOVER_ACCOUNT": "Recuperar conta", + "RECOVERY_KEY_HINT": "Chave de recuperação", + "RECOVER": "Recuperar", + "NO_RECOVERY_KEY": "Não possui a chave de recuperação?", + "INCORRECT_RECOVERY_KEY": "Chave de recuperação incorreta", + "SORRY": "Desculpe", + "NO_RECOVERY_KEY_MESSAGE": "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", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Por favor, envie um e-mail para <a>{{emailID}}<a> a partir do seu endereço de e-mail registrado", + "CONTACT_SUPPORT": "Falar com o suporte", + "REQUEST_FEATURE": "Solicitar Funcionalidade", + "SUPPORT": "Suporte", + "CONFIRM": "Confirmar", + "CANCEL": "Cancelar", + "LOGOUT": "Encerrar sessão", + "DELETE_ACCOUNT": "Excluir conta", + "DELETE_ACCOUNT_MESSAGE": "<p>Por favor, envie um e-mail para <a>{{emailID}}</a> a partir do seu endereço de e-mail registrado.</p><p>Seu pedido será processado dentro de 72 horas.</p>", + "LOGOUT_MESSAGE": "Você tem certeza que deseja encerrar a sessão?", + "CHANGE_EMAIL": "Mudar e-mail", + "OK": "Aceitar", + "SUCCESS": "Bem-sucedido", + "ERROR": "Erro", + "MESSAGE": "Mensagem", + "INSTALL_MOBILE_APP": "Instale nosso aplicativo <a>Android</a> ou <b>iOS</b> para fazer backup automático de todas as suas fotos", + "DOWNLOAD_APP_MESSAGE": "Desculpe, esta operação só é suportada em nosso aplicativo para computador", + "DOWNLOAD_APP": "Baixar aplicativo para computador", + "EXPORT": "Exportar dados", + "SUBSCRIPTION": "Assinatura", + "SUBSCRIBE": "Assinar", + "MANAGEMENT_PORTAL": "Gerenciar métodos de pagamento", + "MANAGE_FAMILY_PORTAL": "Gerenciar Família", + "LEAVE_FAMILY_PLAN": "Sair do plano familiar", + "LEAVE": "Sair", + "LEAVE_FAMILY_CONFIRM": "Tem certeza que deseja sair do plano familiar?", + "CHOOSE_PLAN": "Escolha seu plano", + "MANAGE_PLAN": "Gerenciar sua assinatura", + "ACTIVE": "Ativo", + "OFFLINE_MSG": "Você está offline, memórias em cache estão sendo mostradas", + "FREE_SUBSCRIPTION_INFO": "Você está no plano <strong>gratuito</strong> que expira em {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Você está em um plano familiar gerenciado por", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renovações em {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Termina em {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Sua assinatura será cancelada em {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Seu complemento {{storage, string}} é válido até o dia {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Você excedeu sua cota de armazenamento, por favor <a>atualize</a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>Recebemos o seu pagamento</p><p>Sua assinatura é válida até <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Sua compra foi cancelada, por favor, tente novamente se quiser assinar", + "SUBSCRIPTION_PURCHASE_FAILED": "Falha na compra de assinatura, tente novamente", + "SUBSCRIPTION_UPDATE_FAILED": "Falha ao atualizar assinatura, tente novamente", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Desculpe-nos, o pagamento falhou quando tentamos cobrar o seu cartão, por favor atualize seu método de pagamento e tente novamente", + "STRIPE_AUTHENTICATION_FAILED": "Não foi possível autenticar seu método de pagamento. Por favor, escolha outro método de pagamento e tente novamente", + "UPDATE_PAYMENT_METHOD": "Atualizar forma de pagamento", + "MONTHLY": "Mensal", + "YEARLY": "Anual", + "UPDATE_SUBSCRIPTION_MESSAGE": "Tem certeza que deseja trocar de plano?", + "UPDATE_SUBSCRIPTION": "Mudar de plano", + "CANCEL_SUBSCRIPTION": "Cancelar assinatura", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Todos os seus dados serão excluídos dos nossos servidores no final deste período de cobrança.</p><p>Você tem certeza que deseja cancelar sua assinatura?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Tem certeza que deseja cancelar sua assinatura?</p>", + "SUBSCRIPTION_CANCEL_FAILED": "Falha ao cancelar a assinatura", + "SUBSCRIPTION_CANCEL_SUCCESS": "Assinatura cancelada com sucesso", + "REACTIVATE_SUBSCRIPTION": "Reativar assinatura", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Uma vez reativado, você será cobrado em {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Assinatura ativada com sucesso ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Falha ao reativar as renovações de assinaturas", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Obrigado", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancelar assinatura móvel", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Por favor, cancele sua assinatura do aplicativo móvel para ativar uma assinatura aqui", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Entre em contato com <a>{{emailID}}</a> para gerenciar sua assinatura", + "RENAME": "Renomear", + "RENAME_FILE": "Renomear arquivo", + "RENAME_COLLECTION": "Renomear álbum", + "DELETE_COLLECTION_TITLE": "Excluir álbum?", + "DELETE_COLLECTION": "Excluir álbum", + "DELETE_COLLECTION_MESSAGE": "Também excluir as fotos (e vídeos) presentes neste álbum de <a>todos os</a> outros álbuns dos quais eles fazem parte?", + "DELETE_PHOTOS": "Excluir fotos", + "KEEP_PHOTOS": "Manter fotos", + "SHARE": "Compartilhar", + "SHARE_COLLECTION": "Compartilhar álbum", + "SHAREES": "Compartilhado com", + "SHARE_WITH_SELF": "Você não pode compartilhar consigo mesmo", + "ALREADY_SHARED": "Ops, você já está compartilhando isso com {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Álbum compartilhado não permitido", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Compartilhamento está desabilitado para contas gratuitas", + "DOWNLOAD_COLLECTION": "Baixar álbum", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Tem certeza que deseja baixar o álbum completo?</p><p>Todos os arquivos serão colocados na fila para baixar sequencialmente</p>", + "CREATE_ALBUM_FAILED": "Falha ao criar álbum, por favor tente novamente", + "SEARCH": "Pesquisar", + "SEARCH_RESULTS": "Resultados de pesquisa", + "NO_RESULTS": "Nenhum resultado encontrado", + "SEARCH_HINT": "Pesquisar por álbuns, datas, descrições, ...", + "SEARCH_TYPE": { + "COLLECTION": "Álbum", + "LOCATION": "Local", + "CITY": "Local", + "DATE": "Data", + "FILE_NAME": "Nome do arquivo", + "THING": "Conteúdo", + "FILE_CAPTION": "Descrição", + "FILE_TYPE": "Tipo de arquivo", + "CLIP": "Mágica" + }, + "photos_count_zero": "Sem memórias", + "photos_count_one": "1 memória", + "photos_count_other": "{{count, number}} memórias", + "TERMS_AND_CONDITIONS": "Eu concordo com os <a>termos</a> e <b>a política de privacidade</b>", + "ADD_TO_COLLECTION": "Adicionar ao álbum", + "SELECTED": "selecionado", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Este vídeo não pode ser reproduzido no seu navegador", + "PEOPLE": "Pessoas", + "INDEXING_SCHEDULED": "Indexação está programada...", + "ANALYZING_PHOTOS": "Indexando fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Indexando pessoas em {{indexStatus.nSyncedFiles,number}} fotos...", + "INDEXING_DONE": "Foram indexadas {{indexStatus.nSyncedFiles,number}} fotos", + "UNIDENTIFIED_FACES": "rostos não identificados", + "OBJECTS": "objetos", + "TEXT": "texto", + "INFO": "Informação ", + "INFO_OPTION": "Informação (I)", + "FILE_NAME": "Nome do arquivo", + "CAPTION_PLACEHOLDER": "Adicionar uma descrição", + "LOCATION": "Local", + "SHOW_ON_MAP": "Ver no OpenStreetMap", + "MAP": "Mapa", + "MAP_SETTINGS": "Ajustes do mapa", + "ENABLE_MAPS": "Habilitar mapa?", + "ENABLE_MAP": "Habilitar mapa", + "DISABLE_MAPS": "Desativar Mapas?", + "ENABLE_MAP_DESCRIPTION": "Isto mostrará suas fotos em um mapa do mundo.</p> <p>Este mapa é hospedado pelo <a>OpenStreetMap <a>, e os exatos locais de suas fotos nunca são compartilhados.</p> <p>Você pode desativar esse recurso a qualquer momento nas Configurações.</p>", + "DISABLE_MAP_DESCRIPTION": "<p>Isto irá desativar a exibição de suas fotos em um mapa mundial.</p> <p>Você pode ativar este recurso a qualquer momento nas Configurações.</p>", + "DISABLE_MAP": "Desabilitar mapa", + "DETAILS": "Detalhes", + "VIEW_EXIF": "Ver todos os dados EXIF", + "NO_EXIF": "Sem dados EXIF", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Dois fatores", + "TWO_FACTOR_AUTHENTICATION": "Autenticação de dois fatores", + "TWO_FACTOR_QR_INSTRUCTION": "Digitalize o código QR abaixo com o seu aplicativo de autenticador favorito", + "ENTER_CODE_MANUALLY": "Inserir código manualmente", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Por favor, insira este código no seu aplicativo autenticador favorito", + "SCAN_QR_CODE": "Em vez disso, escaneie um Código QR", + "ENABLE_TWO_FACTOR": "Ativar autenticação de dois fatores", + "ENABLE": "Habilitar", + "LOST_DEVICE": "Dispositivo de dois fatores perdido", + "INCORRECT_CODE": "Código incorreto", + "TWO_FACTOR_INFO": "Adicione uma camada adicional de segurança, exigindo mais do que seu e-mail e senha para entrar na sua conta", + "DISABLE_TWO_FACTOR_LABEL": "Desativar autenticação de dois fatores", + "UPDATE_TWO_FACTOR_LABEL": "Atualize seu dispositivo autenticador", + "DISABLE": "Desativar", + "RECONFIGURE": "Reconfigurar", + "UPDATE_TWO_FACTOR": "Atualizar dois fatores", + "UPDATE_TWO_FACTOR_MESSAGE": "Continuar adiante anulará qualquer autenticador configurado anteriormente", + "UPDATE": "Atualização", + "DISABLE_TWO_FACTOR": "Desativar autenticação de dois fatores", + "DISABLE_TWO_FACTOR_MESSAGE": "Você tem certeza de que deseja desativar a autenticação de dois fatores", + "TWO_FACTOR_DISABLE_FAILED": "Não foi possível desativar dois fatores, por favor tente novamente", + "EXPORT_DATA": "Exportar dados", + "SELECT_FOLDER": "Selecione a pasta", + "DESTINATION": "Destino", + "START": "Iniciar", + "LAST_EXPORT_TIME": "Data da última exportação", + "EXPORT_AGAIN": "Resincronizar", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Armazenamento local não acessível", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Seu navegador ou uma extensão está bloqueando o ente de salvar os dados no armazenamento local. Por favor, tente carregar esta página depois de alternar o modo de navegação.", + "SEND_OTT": "Enviar códigos OTP", + "EMAIl_ALREADY_OWNED": "Este e-mail já está em uso", + "ETAGS_BLOCKED": "<p>Não foi possível fazer o envio dos seguintes arquivos devido à configuração do seu navegador.</p><p>Por favor, desative quaisquer complementos que possam estar impedindo o ente de utilizar <code>eTags</code> para enviar arquivos grandes, ou utilize nosso <a>aplicativo para computador</a> para uma experiência de importação mais confiável.</p>", + "SKIPPED_VIDEOS_INFO": "<p>Atualmente, não oferecemos suporte para adicionar vídeos através de links públicos.</p><p>Para compartilhar vídeos, por favor, faça <a>cadastro</a> no ente e compartilhe com os destinatários pretendidos usando seus e-mails.</p>", + "LIVE_PHOTOS_DETECTED": "Os arquivos de foto e vídeo das suas Fotos em Movimento foram mesclados em um único arquivo", + "RETRY_FAILED": "Repetir envios que falharam", + "FAILED_UPLOADS": "Envios com falhas ", + "SKIPPED_FILES": "Envios ignorados", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Falha ao gerar miniaturas", + "UNSUPPORTED_FILES": "Arquivos não suportados", + "SUCCESSFUL_UPLOADS": "Envios bem sucedidos", + "SKIPPED_INFO": "Ignorar estes como existem arquivos com nomes correspondentes no mesmo álbum", + "UNSUPPORTED_INFO": "ente ainda não suporta estes formatos de arquivo", + "BLOCKED_UPLOADS": "Envios bloqueados", + "SKIPPED_VIDEOS": "Vídeos ignorados", + "INPROGRESS_METADATA_EXTRACTION": "Em andamento", + "INPROGRESS_UPLOADS": "Envios em andamento", + "TOO_LARGE_UPLOADS": "Arquivos grandes", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Armazenamento insuficiente", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Estes arquivos não foram carregados pois excedem o tamanho máximo para seu plano de armazenamento", + "TOO_LARGE_INFO": "Estes arquivos não foram carregados pois excedem nosso limite máximo de tamanho de arquivo", + "THUMBNAIL_GENERATION_FAILED_INFO": "Estes arquivos foram enviados, mas infelizmente não conseguimos gerar as miniaturas para eles.", + "UPLOAD_TO_COLLECTION": "Enviar para o álbum", + "UNCATEGORIZED": "Sem categoria", + "ARCHIVE": "Arquivar", + "FAVORITES": "Favoritos", + "ARCHIVE_COLLECTION": "Arquivar álbum", + "ARCHIVE_SECTION_NAME": "Arquivar", + "ALL_SECTION_NAME": "Todos", + "MOVE_TO_COLLECTION": "Mover para álbum", + "UNARCHIVE": "Desarquivar", + "UNARCHIVE_COLLECTION": "Desarquivar álbum", + "HIDE_COLLECTION": "Ocultar álbum", + "UNHIDE_COLLECTION": "Reexibir álbum", + "MOVE": "Mover", + "ADD": "Adicionar", + "REMOVE": "Remover", + "YES_REMOVE": "Sim, remover", + "REMOVE_FROM_COLLECTION": "Remover do álbum", + "TRASH": "Lixeira", + "MOVE_TO_TRASH": "Mover para a lixeira", + "TRASH_FILES_MESSAGE": "Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo.", + "TRASH_FILE_MESSAGE": "Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo.", + "DELETE_PERMANENTLY": "Excluir permanentemente", + "RESTORE": "Restaurar", + "RESTORE_TO_COLLECTION": "Restaurar para álbum", + "EMPTY_TRASH": "Esvaziar a lixeira", + "EMPTY_TRASH_TITLE": "Esvaziar a lixeira?", + "EMPTY_TRASH_MESSAGE": "Estes arquivos serão excluídos permanentemente da sua conta do ente.", + "LEAVE_SHARED_ALBUM": "Sim, sair", + "LEAVE_ALBUM": "Sair do álbum", + "LEAVE_SHARED_ALBUM_TITLE": "Sair do álbum compartilhado?", + "LEAVE_SHARED_ALBUM_MESSAGE": "Você deixará o álbum e ele deixará de ser visível para você.", + "NOT_FILE_OWNER": "Você não pode excluir arquivos em um álbum compartilhado", + "CONFIRM_SELF_REMOVE_MESSAGE": "Os itens selecionados serão removidos deste álbum. Itens que estão somente neste álbum serão movidos a aba Sem Categoria.", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Alguns dos itens que você está removendo foram adicionados por outras pessoas, e você perderá o acesso a eles.", + "SORT_BY_CREATION_TIME_ASCENDING": "Mais antigo", + "SORT_BY_UPDATION_TIME_DESCENDING": "Última atualização", + "SORT_BY_NAME": "Nome", + "COMPRESS_THUMBNAILS": "Compactar miniaturas", + "THUMBNAIL_REPLACED": "Miniaturas compactadas", + "FIX_THUMBNAIL": "Compactar", + "FIX_THUMBNAIL_LATER": "Compactar depois", + "REPLACE_THUMBNAIL_NOT_STARTED": "Algumas miniaturas de seus vídeos podem ser compactadas para economizar espaço. Você gostaria de compactá-las?", + "REPLACE_THUMBNAIL_COMPLETED": "Miniaturas compactadas com sucesso", + "REPLACE_THUMBNAIL_NOOP": "Você não tem nenhuma miniatura que possa ser compactadas mais", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Não foi possível compactar algumas das suas miniaturas, por favor tente novamente", + "FIX_CREATION_TIME": "Corrigir hora", + "FIX_CREATION_TIME_IN_PROGRESS": "Corrigindo horário", + "CREATION_TIME_UPDATED": "Hora do arquivo atualizado", + "UPDATE_CREATION_TIME_NOT_STARTED": "Selecione a carteira que você deseja usar", + "UPDATE_CREATION_TIME_COMPLETED": "Todos os arquivos atualizados com sucesso", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "A atualização do horário falhou para alguns arquivos, por favor, tente novamente", + "CAPTION_CHARACTER_LIMIT": "5000 caracteres no máximo", + "DATE_TIME_ORIGINAL": "Data e Hora Original", + "DATE_TIME_DIGITIZED": "Data e Hora Digitalizada", + "METADATA_DATE": "Data de Metadados", + "CUSTOM_TIME": "Tempo personalizado", + "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planos", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Falha ao abrir planos", + "INSTALL": "Instalar", + "SHARING_DETAILS": "Detalhes de compartilhamento", + "MODIFY_SHARING": "Modificar compartilhamento", + "ADD_COLLABORATORS": "Adicionar colaboradores", + "ADD_NEW_EMAIL": "Adicionar um novo email", + "shared_with_people_zero": "Compartilhar com pessoas específicas", + "shared_with_people_one": "Compartilhado com 1 pessoa", + "shared_with_people_other": "Compartilhado com {{count, number}} pessoas", + "participants_zero": "Nenhum participante", + "participants_one": "1 participante", + "participants_other": "{{count, number}} participantes", + "ADD_VIEWERS": "Adicionar visualizações", + "PARTICIPANTS": "Participantes", + "CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} Não poderá adicionar mais fotos a este álbum<p><p>Eles ainda poderão remover as fotos existentes adicionadas por eles<p>", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} poderá adicionar fotos ao álbum", + "CONVERT_TO_VIEWER": "Sim, converter para visualizador", + "CONVERT_TO_COLLABORATOR": "Sim, converter para colaborador", + "CHANGE_PERMISSION": "Alterar permissões?", + "REMOVE_PARTICIPANT": "Remover?", + "CONFIRM_REMOVE": "Sim, remover", + "MANAGE": "Gerenciar", + "ADDED_AS": "Adicionado como", + "COLLABORATOR_RIGHTS": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado", + "REMOVE_PARTICIPANT_HEAD": "Remover participante", + "OWNER": "Proprietário", + "COLLABORATORS": "Colaboradores", + "ADD_MORE": "Adicionar mais", + "VIEWERS": "Visualizações", + "OR_ADD_EXISTING": "Ou escolha um existente", + "REMOVE_PARTICIPANT_MESSAGE": "</p>{{selectedEmail}} será removido deste álbum compartilhado</p> <p>Quaisquer fotos adicionadas por eles também serão removidas do álbum</p>", + "NOT_FOUND": "404 Página não encontrada", + "LINK_EXPIRED": "Link expirado", + "LINK_EXPIRED_MESSAGE": "Este link expirou ou foi desativado!", + "MANAGE_LINK": "Gerenciar link", + "LINK_TOO_MANY_REQUESTS": "Desculpe, este álbum foi visualizado em muitos dispositivos!", + "FILE_DOWNLOAD": "Permitir transferências", + "LINK_PASSWORD_LOCK": "Bloqueio de senha", + "PUBLIC_COLLECT": "Permitir adicionar fotos", + "LINK_DEVICE_LIMIT": "Limite de dispositivos", + "NO_DEVICE_LIMIT": "Nenhum", + "LINK_EXPIRY": "Expiração do link", + "NEVER": "Nunca", + "DISABLE_FILE_DOWNLOAD": "Desabilitar transferência", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>Tem certeza de que deseja desativar o botão de download para arquivos?</p><p>Os visualizadores ainda podem capturar imagens da tela ou salvar uma cópia de suas fotos usando ferramentas externas.</p>", + "MALICIOUS_CONTENT": "Contém conteúdo malicioso", + "COPYRIGHT": "Viola os direitos autorais de alguém que estou autorizado a representar", + "SHARED_USING": "Compartilhar usando ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "Use o código <strong>{{referralCode}}</strong> para obter 10 GB de graça", + "LIVE": "AO VIVO", + "DISABLE_PASSWORD": "Desativar bloqueio por senha", + "DISABLE_PASSWORD_MESSAGE": "Tem certeza que deseja desativar o bloqueio por senha?", + "PASSWORD_LOCK": "Bloqueio de senha", + "LOCK": "Bloquear", + "DOWNLOAD_UPLOAD_LOGS": "Logs de depuração", + "UPLOAD_FILES": "Arquivo", + "UPLOAD_DIRS": "Pasta", + "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", + "DEDUPLICATE_FILES": "Arquivos Deduplicados", + "AUTHENTICATOR_SECTION": "Autenticação", + "NO_DUPLICATES_FOUND": "Você não tem arquivos duplicados que possam ser limpos", + "CLUB_BY_CAPTURE_TIME": "Agrupar por tempo de captura", + "FILES": "Arquivos", + "EACH": "Cada", + "DEDUPLICATE_BASED_ON_SIZE": "Os seguintes arquivos foram listados com base em seus tamanhos, por favor, reveja e exclua os itens que você acredita que são duplicados", + "STOP_ALL_UPLOADS_MESSAGE": "Tem certeza que deseja parar todos os envios em andamento?", + "STOP_UPLOADS_HEADER": "Parar envios?", + "YES_STOP_UPLOADS": "Sim, parar envios", + "STOP_DOWNLOADS_HEADER": "Parar transferências?", + "YES_STOP_DOWNLOADS": "Sim, parar transferências", + "STOP_ALL_DOWNLOADS_MESSAGE": "Tem certeza que deseja parar todos as transferências em andamento?", + "albums_one": "1 Álbum", + "albums_other": "{{count, number}} Álbuns", + "ALL_ALBUMS": "Todos os álbuns", + "ALBUMS": "Álbuns", + "ALL_HIDDEN_ALBUMS": "Todos os álbuns ocultos", + "HIDDEN_ALBUMS": "Álbuns ocultos", + "HIDDEN_ITEMS": "Itens ocultos", + "HIDDEN_ITEMS_SECTION_NAME": "Itens_ocultos", + "ENTER_TWO_FACTOR_OTP": "Digite o código de 6 dígitos de\nseu aplicativo autenticador.", + "CREATE_ACCOUNT": "Criar uma conta", + "COPIED": "Copiado", + "CANVAS_BLOCKED_TITLE": "Não foi possível gerar miniatura", + "CANVAS_BLOCKED_MESSAGE": "<p>Parece que o seu navegador desativou o acesso à tela que é necessário para gerar miniaturas para as suas fotos </p> <p> Por favor, habilite o acesso à tela do seu navegador, ou veja nosso aplicativo para computador</p>", + "WATCH_FOLDERS": "Pastas monitoradas", + "UPGRADE_NOW": "Aprimorar agora", + "RENEW_NOW": "Renovar agora", + "STORAGE": "Armazenamento", + "USED": "usado", + "YOU": "Você", + "FAMILY": "Família", + "FREE": "grátis", + "OF": "de", + "WATCHED_FOLDERS": "Pastas monitoradas", + "NO_FOLDERS_ADDED": "Nenhuma pasta adicionada ainda!", + "FOLDERS_AUTOMATICALLY_MONITORED": "As pastas que você adicionar aqui serão monitoradas automaticamente", + "UPLOAD_NEW_FILES_TO_ENTE": "Enviar novos arquivos para o ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Remover arquivos excluídos do ente", + "ADD_FOLDER": "Adicionar pasta", + "STOP_WATCHING": "Parar de acompanhar", + "STOP_WATCHING_FOLDER": "Parar de acompanhar a pasta?", + "STOP_WATCHING_DIALOG_MESSAGE": "Seus arquivos existentes não serão excluídos, mas ente irá parar de atualizar automaticamente o álbum associado em alterações nesta pasta.", + "YES_STOP": "Sim, parar", + "MONTH_SHORT": "mês", + "YEAR": "ano", + "FAMILY_PLAN": "Plano familiar", + "DOWNLOAD_LOGS": "Baixar logs", + "DOWNLOAD_LOGS_MESSAGE": "<p>Isto irá baixar os logs de depuração, que você pode enviar para nós para ajudar a depurar seu problema.</p><p> Por favor, note que os nomes de arquivos serão incluídos para ajudar a rastrear problemas com arquivos específicos. </p>", + "CHANGE_FOLDER": "Alterar pasta", + "TWO_MONTHS_FREE": "Obtenha 2 meses gratuitos em planos anuais", + "GB": "GB", + "POPULAR": "Popular", + "FREE_PLAN_OPTION_LABEL": "Continuar com teste gratuito", + "FREE_PLAN_DESCRIPTION": "1 GB por 1 ano", + "CURRENT_USAGE": "O uso atual é <strong>{{usage}}</strong>", + "WEAK_DEVICE": "O navegador da web que você está usando não é poderoso o suficiente para criptografar suas fotos. Por favor, tente entrar para o ente no computador ou baixe o aplicativo móvel.", + "DRAG_AND_DROP_HINT": "Ou arraste e solte na janela ente", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Seus dados enviados serão agendados para exclusão e sua conta será excluída permanentemente.<br/><br/>Essa ação não é reversível.", + "AUTHENTICATE": "Autenticar", + "UPLOADED_TO_SINGLE_COLLECTION": "Enviado para coleção única", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "Enviada para separar coleções", + "NEVERMIND": "Esquecer", + "UPDATE_AVAILABLE": "Atualização disponível", + "UPDATE_INSTALLABLE_MESSAGE": "Uma nova versão do ente está pronta para ser instalada.", + "INSTALL_NOW": "Instalar agora", + "INSTALL_ON_NEXT_LAUNCH": "Instalar na próxima inicialização", + "UPDATE_AVAILABLE_MESSAGE": "Uma nova versão do ente foi lançada, mas não pode ser baixada e instalada automaticamente.", + "DOWNLOAD_AND_INSTALL": "Baixar e instalar", + "IGNORE_THIS_VERSION": "Ignorar esta versão", + "TODAY": "Hoje", + "YESTERDAY": "Ontem", + "NAME_PLACEHOLDER": "Nome...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Não foi possível criar álbuns a partir da mistura de arquivos/pastas", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Você arrastou e deixou uma mistura de arquivos e pastas.</p><p>Por favor, forneça apenas arquivos ou apenas pastas ao selecionar a opção para criar álbuns separados</p>", + "CHOSE_THEME": "Escolher tema", + "ML_SEARCH": "Reconhecimento facial", + "ENABLE_ML_SEARCH_DESCRIPTION": "<p>Isso permitirá aprendizado de máquina no dispositivo e busca facial, iniciando a análise de suas fotos enviadas localmente.</p><p>Na primeira execução após o login ou habilitação desta funcionalidade, será feito o download de todas as imagens no dispositivo local para análise. Portanto, ative isso apenas se estiver confortável com o consumo de largura de banda e processamento local de todas as imagens em sua biblioteca de fotos.</p><p>Se esta for a primeira vez que você está habilitando isso, também solicitaremos sua permissão para processar dados faciais.</p>", + "ML_MORE_DETAILS": "Mais detalhes", + "ENABLE_FACE_SEARCH": "Habilitar reconhecimento facial", + "ENABLE_FACE_SEARCH_TITLE": "Habilitar reconhecimento facial?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "<p>Se você habilitar o reconhecimento facial, o aplicativo extrairá a geometria do rosto de suas fotos. Isso ocorrerá em seu dispositivo, e quaisquer dados biométricos gerados serão criptografados de ponta a ponta.<p/><p><a>Por favor, clique aqui para obter mais detalhes sobre esta funcionalidade em nossa política de privacidade</a></p>", + "DISABLE_BETA": "Pausar reconhecimento", + "DISABLE_FACE_SEARCH": "Desativar reconhecimento facial", + "DISABLE_FACE_SEARCH_TITLE": "Desativar reconhecimento facial?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "<p>Ente irá parar de processar geometria facial.</p><p>Você pode reativar o reconhecimento facial novamente, se desejar, então esta operação está segura.</p>", + "ADVANCED": "Avançado", + "FACE_SEARCH_CONFIRMATION": "Eu entendo, e desejo permitir que o ente processe a geometria do rosto", + "LABS": "Laboratórios", + "YOURS": "seu", + "PASSPHRASE_STRENGTH_WEAK": "Força da senha: fraca", + "PASSPHRASE_STRENGTH_MODERATE": "Força da senha: moderada", + "PASSPHRASE_STRENGTH_STRONG": "Força da senha: forte", + "PREFERENCES": "Preferências", + "LANGUAGE": "Idioma", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Diretório de exportação inválido", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>O diretório de exportação que você selecionou não existe.</p><p> Por favor, selecione um diretório válido.</p>", + "SUBSCRIPTION_VERIFICATION_ERROR": "Falha na verificação de assinatura", + "STORAGE_UNITS": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "após uma hora", + "DAY": "após um dia", + "WEEK": "após uma semana", + "MONTH": "após um mês", + "YEAR": "após um ano" + }, + "COPY_LINK": "Copiar link", + "DONE": "Concluído", + "LINK_SHARE_TITLE": "Ou compartilhe um link", + "REMOVE_LINK": "Remover link", + "CREATE_PUBLIC_SHARING": "Criar link público", + "PUBLIC_LINK_CREATED": "Link público criado", + "PUBLIC_LINK_ENABLED": "Link público ativado", + "COLLECT_PHOTOS": "Coletar fotos", + "PUBLIC_COLLECT_SUBTEXT": "Permita que as pessoas com o link também adicionem fotos ao álbum compartilhado.", + "STOP_EXPORT": "Parar", + "EXPORT_PROGRESS": "<a>{{progress.success, number}} / {{progress.total, number}}</a> itens sincronizados", + "MIGRATING_EXPORT": "Preparando...", + "RENAMING_COLLECTION_FOLDERS": "Renomeando pastas do álbum...", + "TRASHING_DELETED_FILES": "Descartando arquivos excluídos...", + "TRASHING_DELETED_COLLECTIONS": "Descartando álbuns excluídos...", + "EXPORT_NOTIFICATION": { + "START": "Exportação iniciada", + "IN_PROGRESS": "Exportação já em andamento", + "FINISH": "Exportação finalizada", + "UP_TO_DATE": "Não há arquivos novos para exportar" + }, + "CONTINUOUS_EXPORT": "Sincronizar continuamente", + "TOTAL_ITEMS": "Total de itens", + "PENDING_ITEMS": "Itens pendentes", + "EXPORT_STARTING": "Iniciando a exportação...", + "DELETE_ACCOUNT_REASON_LABEL": "Qual é o principal motivo para você excluir sua conta?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Selecione um motivo", + "DELETE_REASON": { + "MISSING_FEATURE": "Está faltando um recurso que eu preciso", + "BROKEN_BEHAVIOR": "O aplicativo ou um determinado recurso não está funcionando como eu acredito que deveria", + "FOUND_ANOTHER_SERVICE": "Encontrei outro serviço que gosto mais", + "NOT_LISTED": "Meu motivo não está listado" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "Sentimos muito em vê-lo partir. Explique por que você está partindo para nos ajudar a melhorar.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Comentários", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Sim, desejo excluir permanentemente esta conta e todos os seus dados", + "CONFIRM_DELETE_ACCOUNT": "Confirmar exclusão da conta", + "FEEDBACK_REQUIRED": "Por favor, ajude-nos com esta informação", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "O que o outro serviço faz melhor?", + "RECOVER_TWO_FACTOR": "Recuperar dois fatores", + "at": "em", + "AUTH_NEXT": "próximo", + "AUTH_DOWNLOAD_MOBILE_APP": "Baixe nosso aplicativo móvel para gerenciar seus segredos", + "HIDDEN": "Escondido", + "HIDE": "Ocultar", + "UNHIDE": "Desocultar", + "UNHIDE_TO_COLLECTION": "Reexibir para o álbum", + "SORT_BY": "Ordenar por", + "NEWEST_FIRST": "Mais recentes primeiro", + "OLDEST_FIRST": "Mais antigo primeiro", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Este arquivo não pôde ser pré-visualizado. Clique aqui para baixar o original.", + "SELECT_COLLECTION": "Selecionar álbum", + "PIN_ALBUM": "Fixar álbum", + "UNPIN_ALBUM": "Desafixar álbum", + "DOWNLOAD_COMPLETE": "Transferência concluída", + "DOWNLOADING_COLLECTION": "Transferindo {{name}}", + "DOWNLOAD_FAILED": "Falha ao baixar", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", + "CHRISTMAS": "Natal", + "CHRISTMAS_EVE": "Véspera de Natal", + "NEW_YEAR": "Ano Novo", + "NEW_YEAR_EVE": "Véspera de Ano Novo", + "IMAGE": "Imagem", + "VIDEO": "Vídeo", + "LIVE_PHOTO": "Fotos em movimento", + "CONVERT": "Converter", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Tem certeza de que deseja fechar o editor?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Baixe sua imagem editada ou salve uma cópia para o ente para persistir nas alterações.", + "BRIGHTNESS": "Brilho", + "CONTRAST": "Contraste", + "SATURATION": "Saturação", + "BLUR": "Desfoque", + "INVERT_COLORS": "Inverter Cores", + "ASPECT_RATIO": "Proporção da imagem", + "SQUARE": "Quadrado", + "ROTATE_LEFT": "Girar para a Esquerda", + "ROTATE_RIGHT": "Girar para a Direita", + "FLIP_VERTICALLY": "Inverter verticalmente", + "FLIP_HORIZONTALLY": "Inverter horizontalmente", + "DOWNLOAD_EDITED": "Transferência Editada", + "SAVE_A_COPY_TO_ENTE": "Salvar uma cópia para o ente", + "RESTORE_ORIGINAL": "Restaurar original", + "TRANSFORM": "Transformar", + "COLORS": "Cores", + "FLIP": "Inverter", + "ROTATION": "Rotação", + "RESET": "Redefinir", + "PHOTO_EDITOR": "Editor de Fotos", + "FASTER_UPLOAD": "Envios mais rápidos", + "FASTER_UPLOAD_DESCRIPTION": "Rotas enviam em servidores próximos", + "MAGIC_SEARCH_STATUS": "Estado da busca mágica", + "INDEXED_ITEMS": "Itens indexados", + "CAST_ALBUM_TO_TV": "Reproduzir álbum na TV", + "ENTER_CAST_PIN_CODE": "Digite o código que você vê na TV abaixo para parear este dispositivo.", + "PAIR_DEVICE_TO_TV": "Parear dispositivos", + "TV_NOT_FOUND": "TV não encontrada. Você inseriu o PIN correto?", + "AUTO_CAST_PAIR": "Pareamento automático", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "O Auto Pair requer a conexão com servidores do Google e só funciona com dispositivos Chromecast. O Google não receberá dados confidenciais, como suas fotos.", + "PAIR_WITH_PIN": "Parear com PIN", + "CHOOSE_DEVICE_FROM_BROWSER": "Escolha um dispositivo compatível com casts no navegador popup.", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Parear com o PIN funciona para qualquer dispositivo de tela grande onde você deseja reproduzir seu álbum.", + "VISIT_CAST_ENTE_IO": "Acesse cast.ente.io no dispositivo que você deseja parear.", + "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.", + "CACHE_DIRECTORY": "Pasta de Cache", + "FREEHAND": "Mão livre", + "APPLY_CROP": "Aplicar Recorte", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", + "PASSKEYS": "Chaves de acesso", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/pt-PT/translation.json b/web/apps/cast/public/locales/pt-PT/translation.json new file mode 100644 index 000000000..230980326 --- /dev/null +++ b/web/apps/cast/public/locales/pt-PT/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Backups privados</div><div>para as suas memórias</div>", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "<div>Disponível</div><div> em qualquer lugar</div>", + "HERO_SLIDE_3": "Android, iOS, Web, Desktop", + "LOGIN": "Entrar", + "SIGN_UP": "Registar", + "NEW_USER": "Novo no ente", + "EXISTING_USER": "Utilizador existente", + "ENTER_NAME": "Insira o nome", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", + "ENTER_EMAIL": "Insira o endereço de email", + "EMAIL_ERROR": "Inserir um endereço de email válido", + "REQUIRED": "Obrigatório", + "EMAIL_SENT": "Código de verificação enviado para <a>{{email}}</a>", + "CHECK_INBOX": "Verifique a sua caixa de entrada (e spam) para concluir a verificação", + "ENTER_OTT": "Código de verificação", + "RESEND_MAIL": "Reenviar código", + "VERIFY": "Verificar", + "UNKNOWN_ERROR": "Ocorreu um erro. Tente novamente", + "INVALID_CODE": "Código de verificação inválido", + "EXPIRED_CODE": "O seu código de verificação expirou", + "SENDING": "A enviar...", + "SENT": "Enviado!", + "PASSWORD": "Palavra-passe", + "LINK_PASSWORD": "Introduza a palavra-passe para desbloquear o álbum", + "RETURN_PASSPHRASE_HINT": "Palavra-passe", + "SET_PASSPHRASE": "Definir palavra-passe", + "VERIFY_PASSPHRASE": "Entrar", + "INCORRECT_PASSPHRASE": "Palavra-passe incorreta", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "Bem-vindo ao <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "Novo álbum", + "ENTER_ALBUM_NAME": "Nome do álbum", + "CLOSE_OPTION": "Fechar (Esc)", + "ENTER_FILE_NAME": "Nome do ficheiro", + "CLOSE": "Fechar", + "NO": "Não", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "Importar", + "ADD_PHOTOS": "Adicionar fotos", + "ADD_MORE_PHOTOS": "Adicionar mais fotos", + "add_photos_one": "Adicionar item", + "add_photos_other": "Adicionar {{count, number}} itens", + "SELECT_PHOTOS": "Selecionar fotos", + "FILE_UPLOAD": "Enviar Ficheiro", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "Não possui uma conta", + "ACCOUNT_EXISTS": "Já possui uma conta", + "CREATE": "Criar", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/ru-RU/translation.json b/web/apps/cast/public/locales/ru-RU/translation.json new file mode 100644 index 000000000..c85db2236 --- /dev/null +++ b/web/apps/cast/public/locales/ru-RU/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>Личные резервные копии</div><div>для твоих воспоминаний</div>", + "HERO_SLIDE_1": "Сквозное шифрование по умолчанию", + "HERO_SLIDE_2_TITLE": "<div>Надежно хранится</div><div>в убежище от радиоактивных осадков</div>", + "HERO_SLIDE_2": "Созданный для того, чтобы пережить", + "HERO_SLIDE_3_TITLE": "<div>Доступно</div><div> везде</div>", + "HERO_SLIDE_3": "Android, iOS, Веб, ПК", + "LOGIN": "Авторизоваться", + "SIGN_UP": "Регистрация", + "NEW_USER": "Новенький в ente", + "EXISTING_USER": "Существующий пользователь", + "ENTER_NAME": "Введите имя", + "PUBLIC_UPLOADER_NAME_MESSAGE": "Добавьте имя, чтобы ваши друзья знали, кого благодарить за эти замечательные фотографии!", + "ENTER_EMAIL": "Введите адрес электронной почты", + "EMAIL_ERROR": "Введите действительный адрес электронной почты", + "REQUIRED": "Требуется", + "EMAIL_SENT": "Проверочный код отправлен на <a>{{email}}</a>", + "CHECK_INBOX": "Пожалуйста, проверьте свой почтовый ящик (и спам) для завершения проверки", + "ENTER_OTT": "Проверочный код", + "RESEND_MAIL": "Отправить код еще раз", + "VERIFY": "Подтвердить", + "UNKNOWN_ERROR": "Что-то пошло не так, Попробуйте еще раз", + "INVALID_CODE": "Неверный код подтверждения", + "EXPIRED_CODE": "Срок действия вашего проверочного кода истек", + "SENDING": "Отправка...", + "SENT": "Отправлено!", + "PASSWORD": "Пароль", + "LINK_PASSWORD": "Введите пароль, чтобы разблокировать альбом", + "RETURN_PASSPHRASE_HINT": "Пароль", + "SET_PASSPHRASE": "Установить пароль", + "VERIFY_PASSPHRASE": "Войти", + "INCORRECT_PASSPHRASE": "Неверный пароль", + "ENTER_ENC_PASSPHRASE": "Пожалуйста, введите пароль, который мы можем использовать для шифрования ваших данных", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "Добро пожаловать в <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "Где живут ваши лучшие фотографии", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Генерируем ключи шифрования...", + "PASSPHRASE_HINT": "Пароль", + "CONFIRM_PASSPHRASE": "Подтвердите пароль", + "REFERRAL_CODE_HINT": "Как вы узнали о Ente? (необязательно)", + "REFERRAL_INFO": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения!", + "PASSPHRASE_MATCH_ERROR": "Пароли не совпадают", + "CREATE_COLLECTION": "Новый альбом", + "ENTER_ALBUM_NAME": "Название альбома", + "CLOSE_OPTION": "Закрыть (Esc)", + "ENTER_FILE_NAME": "Имя файла", + "CLOSE": "Закрыть", + "NO": "Нет", + "NOTHING_HERE": "Здесь нечего смотреть! 👀", + "UPLOAD": "Загрузить", + "IMPORT": "Импорт", + "ADD_PHOTOS": "Добавить фотографии", + "ADD_MORE_PHOTOS": "Добавить больше фото", + "add_photos_one": "Добавить 1 элемент", + "add_photos_other": "Добавить {{count, number}} элементов", + "SELECT_PHOTOS": "Выбрать фотографии", + "FILE_UPLOAD": "Загрузка файла", + "UPLOAD_STAGE_MESSAGE": { + "0": "Подготовка к загрузке", + "1": "Чтение файлов метаданных Google", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} файлов извлечены", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} файлов обработано", + "4": "Отмена оставшихся загрузок", + "5": "Резервное копирование завершено" + }, + "FILE_NOT_UPLOADED_LIST": "Следующие файлы не были загружены", + "SUBSCRIPTION_EXPIRED": "Подписка закончилась", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Срок действия вашей подписки истек, пожалуйста, <a>продлите</a>", + "STORAGE_QUOTA_EXCEEDED": "Превышен лимит хранения", + "INITIAL_LOAD_DELAY_WARNING": "Первая загрузка может занять некоторое время", + "USER_DOES_NOT_EXIST": "Пользователь с таким email не найден", + "NO_ACCOUNT": "У вас нет учетной записи", + "ACCOUNT_EXISTS": "Уже есть аккаунт", + "CREATE": "Создать", + "DOWNLOAD": "Скачать", + "DOWNLOAD_OPTION": "Скачать (D)", + "DOWNLOAD_FAVORITES": "Скачать избранные", + "DOWNLOAD_UNCATEGORIZED": "Скачать без категорий", + "DOWNLOAD_HIDDEN_ITEMS": "Скачать скрытые элементы", + "COPY_OPTION": "Скопировать как PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "Полноэкранный режим (F)", + "ZOOM_IN_OUT": "Увеличить/уменьшить", + "PREVIOUS": "Предыдущий (←)", + "NEXT": "Следующий (→)", + "TITLE_PHOTOS": "Ente Фото", + "TITLE_ALBUMS": "Ente Фото", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "Загрузите своё первое фото", + "IMPORT_YOUR_FOLDERS": "Импортируйте папки", + "UPLOAD_DROPZONE_MESSAGE": "Перетащите для резервного копирования файлов", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Перетащите, чтобы добавить просматриваемую папку", + "TRASH_FILES_TITLE": "Удалить файлы?", + "TRASH_FILE_TITLE": "Удалить файл?", + "DELETE_FILES_TITLE": "Удалить немедленно?", + "DELETE_FILES_MESSAGE": "Выбранные файлы будут безвозвратно удалены из вашей учетной записи ente.", + "DELETE": "Удалить", + "DELETE_OPTION": "Удалить (DEL)", + "FAVORITE_OPTION": "Избранное (L)", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "Обнаружено несколько папок", + "UPLOAD_STRATEGY_CHOICE": "Вы хотите загрузить их в", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Один альбом", + "OR": "или", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Отдельные альбомы", + "SESSION_EXPIRED_MESSAGE": "Истёк срок действия вашей сессии. Для продолжения, пожалуйста, войдите снова", + "SESSION_EXPIRED": "Время сессии истекло", + "PASSWORD_GENERATION_FAILED": "Вашему браузеру не удалось сгенерировать надежный ключ, соответствующий стандартам шифрования ente, пожалуйста, попробуйте использовать мобильное приложение или другой браузер", + "CHANGE_PASSWORD": "Изменить пароль", + "GO_BACK": "Вернуться назад", + "RECOVERY_KEY": "Ключ восстановления", + "SAVE_LATER": "Сделать позже", + "SAVE": "Сохранить ключ", + "RECOVERY_KEY_DESCRIPTION": "Если вы забыли свой пароль, то восстановить данные можно только с помощью этого ключа.", + "RECOVER_KEY_GENERATION_FAILED": "Не удалось сгенерировать код восстановления, пожалуйста, повторите попытку", + "KEY_NOT_STORED_DISCLAIMER": "Мы не храним этот ключ, поэтому, пожалуйста, сохраните его в надежном месте", + "FORGOT_PASSWORD": "Забыл пароль", + "RECOVER_ACCOUNT": "Восстановить аккаунт", + "RECOVERY_KEY_HINT": "Ключ восстановления", + "RECOVER": "Восстановить", + "NO_RECOVERY_KEY": "Нет ключа восстановления?", + "INCORRECT_RECOVERY_KEY": "Неправильный ключ восстановления", + "SORRY": "Извините", + "NO_RECOVERY_KEY_MESSAGE": "Из-за природы нашего сквозного протокола шифрования ваши данные не могут быть расшифрованы без вашего пароля или ключа восстановления", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Пожалуйста, отправьте электронное письмо на адрес <a>{{emailID}}</a> с вашего зарегистрированного адреса электронной почты", + "CONTACT_SUPPORT": "Связаться с поддержкой", + "REQUEST_FEATURE": "Запросить функцию", + "SUPPORT": "Поддержка", + "CONFIRM": "Подтвердить", + "CANCEL": "Отменить", + "LOGOUT": "Выйти", + "DELETE_ACCOUNT": "Удалить аккаунт", + "DELETE_ACCOUNT_MESSAGE": "<p>Пожалуйста, отправьте письмо по адресу <a>{{emailID}}</a> с вашего зарегистрированного адреса электронной почты.</p><p> Ваш запрос будет обработан в течение 72 часов</p>", + "LOGOUT_MESSAGE": "Вы уверены, что хотите выйти?", + "CHANGE_EMAIL": "Изменить адрес электронной почты", + "OK": "ОК", + "SUCCESS": "Успешно", + "ERROR": "Ошибка", + "MESSAGE": "Сообщение", + "INSTALL_MOBILE_APP": "Установите наше приложение <a>Android</a> или <b>iOS</b> для автоматического резервного копирования всех ваших фотографий", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "Загрузить приложение для компьютера", + "EXPORT": "Экспортировать данные", + "SUBSCRIPTION": "Подписка", + "SUBSCRIBE": "Подписаться", + "MANAGEMENT_PORTAL": "Управлять платёжной информацией", + "MANAGE_FAMILY_PORTAL": "Управление семьёй", + "LEAVE_FAMILY_PLAN": "Покинуть семейный план", + "LEAVE": "Выйти", + "LEAVE_FAMILY_CONFIRM": "Вы уверены, что хотите покинуть семейный план?", + "CHOOSE_PLAN": "Выбери свой план", + "MANAGE_PLAN": "Управление подпиской", + "ACTIVE": "Активный", + "OFFLINE_MSG": "Вы не в сети, кэшированные воспоминания отображаются", + "FREE_SUBSCRIPTION_INFO": "Вы используете <strong>бесплатный</strong> тарифный план, истекающий {{date, dateTime}}", + "FAMILY_SUBSCRIPTION_INFO": "Вы используете семейный план, управляемый", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Продление {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "Обновить платёжную информацию", + "MONTHLY": "Ежемесячно", + "YEARLY": "Ежегодно", + "UPDATE_SUBSCRIPTION_MESSAGE": "Хотите сменить текущий план?", + "UPDATE_SUBSCRIPTION": "Изменить план", + "CANCEL_SUBSCRIPTION": "Отменить подписку", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>Все ваши данные будут удалены с наших серверов в конце этого расчетного периода.</p><p> Вы уверены, что хотите отменить свою подписку?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Вы уверены, что хотите отменить свою подписку?</p>", + "SUBSCRIPTION_CANCEL_FAILED": "Не удалось отменить подписку", + "SUBSCRIPTION_CANCEL_SUCCESS": "Подписка успешно отменена", + "REACTIVATE_SUBSCRIPTION": "Возобновить подписку", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "После повторной активации вам будет выставлен счет в {{date, dateTime}}", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Подписка успешно активирована ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Не удалось повторно активировать продление подписки", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Спасибо", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Отменить мобильную подписку", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Пожалуйста, отмените свою подписку в мобильном приложении, чтобы активировать подписку здесь", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Пожалуйста, свяжитесь с <a>{{emailID}}</a> для управления подпиской", + "RENAME": "Переименовать", + "RENAME_FILE": "Переименовать файл", + "RENAME_COLLECTION": "Переименовать альбом", + "DELETE_COLLECTION_TITLE": "Удалить альбом?", + "DELETE_COLLECTION": "Удалить альбом", + "DELETE_COLLECTION_MESSAGE": "Также удалить фотографии (и видео), которые есть в этом альбоме из <a>всех</a> других альбомов, где они есть?", + "DELETE_PHOTOS": "Удалить фото", + "KEEP_PHOTOS": "Оставить фото", + "SHARE": "Поделиться", + "SHARE_COLLECTION": "Поделиться альбомом", + "SHAREES": "Поделиться с", + "SHARE_WITH_SELF": "Ой, Вы не можете поделиться с самим собой", + "ALREADY_SHARED": "Упс, Вы уже делились этим с {{email}}", + "SHARING_BAD_REQUEST_ERROR": "Делиться альбомом запрещено", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Совместное использование отключено для бесплатных аккаунтов", + "DOWNLOAD_COLLECTION": "Загрузить альбом", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>Вы уверены, что хотите загрузить альбом полностью?</p><p> Все файлы будут последовательно помещены в очередь на загрузку</p>", + "CREATE_ALBUM_FAILED": "Не удалось создать альбом, пожалуйста, попробуйте еще раз", + "SEARCH": "Поиск", + "SEARCH_RESULTS": "Результаты поиска", + "NO_RESULTS": "Ничего не найдено", + "SEARCH_HINT": "Поиск альбомов, дат, описаний, ...", + "SEARCH_TYPE": { + "COLLECTION": "Альбом", + "LOCATION": "Местоположение", + "CITY": "Местоположение", + "DATE": "Дата", + "FILE_NAME": "Имя файла", + "THING": "Содержимое", + "FILE_CAPTION": "Описание", + "FILE_TYPE": "Тип файла", + "CLIP": "" + }, + "photos_count_zero": "Воспоминания отсутствуют", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "Добавить в альбом", + "SELECTED": "выбрано", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Это видео нельзя воспроизвести в вашем браузере", + "PEOPLE": "Люди", + "INDEXING_SCHEDULED": "Индексация запланирована...", + "ANALYZING_PHOTOS": "Индексирование фотографий ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Индексирование людей на {{indexStatus.nSyncedFiles,number}} фотографиях...", + "INDEXING_DONE": "Проиндексировано {{indexStatus.nSyncedFiles,number}} фотографий", + "UNIDENTIFIED_FACES": "нераспознанные лица", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "Двухфакторная аутентификация", + "TWO_FACTOR_QR_INSTRUCTION": "Сканируйте QR-код ниже с вашим любимым приложением для проверки подлинности", + "ENTER_CODE_MANUALLY": "Введите код вручную", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Пожалуйста, введите этот код в вашем любимом приложении для аутентификации", + "SCAN_QR_CODE": "Сканировать QR-код вместо", + "ENABLE_TWO_FACTOR": "Включить двухфакторную аутентификацию", + "ENABLE": "Включить", + "LOST_DEVICE": "Потеряно двухфакторное устройство", + "INCORRECT_CODE": "Неверный код", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "Отключить двухфакторную аутентификацию", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "Отключить", + "RECONFIGURE": "Перенастроить", + "UPDATE_TWO_FACTOR": "Обновить двухфакторную аутентификацию", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "Обновить", + "DISABLE_TWO_FACTOR": "Отключить двухфакторную аутентификацию", + "DISABLE_TWO_FACTOR_MESSAGE": "Вы уверены, что хотите отключить двухфакторную аутентификацию", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "Экспортировать данные", + "SELECT_FOLDER": "Выбрать папку", + "DESTINATION": "Место назначения", + "START": "Начать", + "LAST_EXPORT_TIME": "Время последнего экспорта", + "EXPORT_AGAIN": "Синхронизировать заново", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Локальное хранилище недоступно", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "Отправить одноразовый код", + "EMAIl_ALREADY_OWNED": "Почта уже использована", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "Скрыть", + "UNHIDE": "Показать", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "Сортировать по", + "NEWEST_FIRST": "Сначала новые", + "OLDEST_FIRST": "Сначала старые", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "Выбрать альбом", + "PIN_ALBUM": "Закрепить альбом", + "UNPIN_ALBUM": "Открепить альбом", + "DOWNLOAD_COMPLETE": "Загрузка завершена", + "DOWNLOADING_COLLECTION": "Загрузка {{name}}", + "DOWNLOAD_FAILED": "Загрузка не удалась", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} файлов", + "CHRISTMAS": "Рождество", + "CHRISTMAS_EVE": "Канун Рождества", + "NEW_YEAR": "Новый год", + "NEW_YEAR_EVE": "Канун Нового года", + "IMAGE": "Изображение", + "VIDEO": "Видео", + "LIVE_PHOTO": "Живое фото", + "CONVERT": "Преобразовать", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Вы уверены, что хотите закрыть редактор?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Загрузите отредактированное изображение или сохраните копию в ente, чтобы сохранить внесенные изменения.", + "BRIGHTNESS": "Яркость", + "CONTRAST": "Контраст", + "SATURATION": "Насыщенность", + "BLUR": "Размытие", + "INVERT_COLORS": "Инвертировать Цвета", + "ASPECT_RATIO": "Соотношение Сторон", + "SQUARE": "Квадрат", + "ROTATE_LEFT": "Повернуть влево", + "ROTATE_RIGHT": "Повернуть вправо", + "FLIP_VERTICALLY": "Отразить вертикально", + "FLIP_HORIZONTALLY": "Отразить горизонтально", + "DOWNLOAD_EDITED": "Скачать отредактированный", + "SAVE_A_COPY_TO_ENTE": "Сохранить копию в ente", + "RESTORE_ORIGINAL": "Восстановить оригинал", + "TRANSFORM": "Преобразовать", + "COLORS": "Цвета", + "FLIP": "Перевернуть", + "ROTATION": "", + "RESET": "Сбросить", + "PHOTO_EDITOR": "Редактор фото", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "Статус волшебного поиска", + "INDEXED_ITEMS": "Индексированные элементы", + "CAST_ALBUM_TO_TV": "Воспроизвести альбом на ТВ", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/sv-SE/translation.json b/web/apps/cast/public/locales/sv-SE/translation.json new file mode 100644 index 000000000..f88535795 --- /dev/null +++ b/web/apps/cast/public/locales/sv-SE/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "Ange namn", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "Ange e-postadress", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "Lösenord", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "Lösenord", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "Logga in", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "Välkommen till <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "Lösenord", + "CONFIRM_PASSPHRASE": "Bekräfta lösenord", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "Lösenorden matchar inte", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "Filnamn", + "CLOSE": "Stäng", + "NO": "Nej", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "Radera", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "eller", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "Ändra lösenord", + "GO_BACK": "", + "RECOVERY_KEY": "Återställningsnyckel", + "SAVE_LATER": "", + "SAVE": "Spara nyckel", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "Glömt lösenord", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "Ingen återställningsnyckel?", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "Bekräfta", + "CANCEL": "Avbryt", + "LOGOUT": "Logga ut", + "DELETE_ACCOUNT": "Radera konto", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "Meddelande", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "Prenumerera", + "MANAGEMENT_PORTAL": "Hantera betalningsmetod", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "Hantera din prenumeration", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "Sök", + "SEARCH_RESULTS": "Sökresultat", + "NO_RESULTS": "Inga resultat hittades", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "Datum", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "Beskrivning", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "Inga deltagare", + "participants_one": "1 deltagare", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "Deltagare", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "Fil", + "UPLOAD_DIRS": "Mapp", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "Filer", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "1 album", + "albums_other": "{{count, number}} album", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "Lägg till mapp", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "mån", + "YEAR": "år", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "GB", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "Namn...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "Språk", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "Kopiera länk", + "DONE": "Klar", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "Sortera efter", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "Bild", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "Färger", + "FLIP": "", + "ROTATION": "", + "RESET": "Återställ", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/th-TH/translation.json b/web/apps/cast/public/locales/th-TH/translation.json new file mode 100644 index 000000000..888ed7093 --- /dev/null +++ b/web/apps/cast/public/locales/th-TH/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/tr-TR/translation.json b/web/apps/cast/public/locales/tr-TR/translation.json new file mode 100644 index 000000000..888ed7093 --- /dev/null +++ b/web/apps/cast/public/locales/tr-TR/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "", + "HERO_SLIDE_1": "", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "DOWNLOAD_FAVORITES": "", + "DOWNLOAD_UNCATEGORIZED": "", + "DOWNLOAD_HIDDEN_ITEMS": "", + "COPY_OPTION": "", + "TOGGLE_FULLSCREEN": "", + "ZOOM_IN_OUT": "", + "PREVIOUS": "", + "NEXT": "", + "TITLE_PHOTOS": "", + "TITLE_ALBUMS": "", + "TITLE_AUTH": "", + "UPLOAD_FIRST_PHOTO": "", + "IMPORT_YOUR_FOLDERS": "", + "UPLOAD_DROPZONE_MESSAGE": "", + "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "TRASH_FILES_TITLE": "", + "TRASH_FILE_TITLE": "", + "DELETE_FILES_TITLE": "", + "DELETE_FILES_MESSAGE": "", + "DELETE": "", + "DELETE_OPTION": "", + "FAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "", + "MULTI_FOLDER_UPLOAD": "", + "UPLOAD_STRATEGY_CHOICE": "", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", + "OR": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "SESSION_EXPIRED_MESSAGE": "", + "SESSION_EXPIRED": "", + "PASSWORD_GENERATION_FAILED": "", + "CHANGE_PASSWORD": "", + "GO_BACK": "", + "RECOVERY_KEY": "", + "SAVE_LATER": "", + "SAVE": "", + "RECOVERY_KEY_DESCRIPTION": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "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": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "CAST_ALBUM_TO_TV": "", + "ENTER_CAST_PIN_CODE": "", + "PAIR_DEVICE_TO_TV": "", + "TV_NOT_FOUND": "", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "PAIR_WITH_PIN": "", + "CHOOSE_DEVICE_FROM_BROWSER": "", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "VISIT_CAST_ENTE_IO": "", + "CAST_AUTO_PAIR_FAILED": "", + "CACHE_DIRECTORY": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/public/locales/zh-CN/translation.json b/web/apps/cast/public/locales/zh-CN/translation.json new file mode 100644 index 000000000..0a72e1c70 --- /dev/null +++ b/web/apps/cast/public/locales/zh-CN/translation.json @@ -0,0 +1,654 @@ +{ + "HERO_SLIDE_1_TITLE": "<div>私人备份</div><div>为您的回忆</div>", + "HERO_SLIDE_1": "默认端到端加密", + "HERO_SLIDE_2_TITLE": "<div>安全地存放</div><div>在一个掩护所中</div>", + "HERO_SLIDE_2": "经久耐用", + "HERO_SLIDE_3_TITLE": "<div>可用于</div><div> 各处</div>", + "HERO_SLIDE_3": "安卓, iOS, 网页端, 桌面端", + "LOGIN": "登录", + "SIGN_UP": "注册", + "NEW_USER": "刚来到 ente", + "EXISTING_USER": "现有用户", + "ENTER_NAME": "输入名字", + "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", + "ENTER_EMAIL": "请输入电子邮件地址", + "EMAIL_ERROR": "请输入有效的电子邮件", + "REQUIRED": "必需的", + "EMAIL_SENT": "验证码已发送至 <a>{{email}}</a>", + "CHECK_INBOX": "请检查您的收件箱 (或者是在您的“垃圾邮件”列表内) 以完成验证", + "ENTER_OTT": "验证码", + "RESEND_MAIL": "重新发送验证码", + "VERIFY": "验证", + "UNKNOWN_ERROR": "出了点问题,请重试", + "INVALID_CODE": "验证码无效", + "EXPIRED_CODE": "您的验证码已过期", + "SENDING": "发送中……", + "SENT": "已发送!", + "PASSWORD": "密码", + "LINK_PASSWORD": "输入密码来解锁相册", + "RETURN_PASSPHRASE_HINT": "密码", + "SET_PASSPHRASE": "设置密码", + "VERIFY_PASSPHRASE": "登录", + "INCORRECT_PASSPHRASE": "密码错误", + "ENTER_ENC_PASSPHRASE": "请输入我们可以用来加密您数据的密码", + "PASSPHRASE_DISCLAIMER": "我们不会存储您的密码,因此如果您忘记密码, <strong>我们将无法帮助您</strong>在没有恢复密钥的情况下恢复您的数据。", + "WELCOME_TO_ENTE_HEADING": "欢迎来到 <a/>", + "WELCOME_TO_ENTE_SUBHEADING": "端到端加密的照片存储和共享", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "可以让您存放照片的最好的地方", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "正在生成加密密钥...", + "PASSPHRASE_HINT": "密码", + "CONFIRM_PASSPHRASE": "请确认密码", + "REFERRAL_CODE_HINT": "您是如何知道Ente的? (可选的)", + "REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", + "PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致", + "CREATE_COLLECTION": "新建相册", + "ENTER_ALBUM_NAME": "相册名称", + "CLOSE_OPTION": "关闭 (或按Esc键)", + "ENTER_FILE_NAME": "文件名", + "CLOSE": "关闭", + "NO": "否", + "NOTHING_HERE": "这里空空如也 👀", + "UPLOAD": "上传", + "IMPORT": "导入", + "ADD_PHOTOS": "添加照片", + "ADD_MORE_PHOTOS": "添加更多的照片", + "add_photos_one": "添加1个项目", + "add_photos_other": "添加 {{count, number}} 个项目", + "SELECT_PHOTOS": "选择图片", + "FILE_UPLOAD": "上传文件", + "UPLOAD_STAGE_MESSAGE": { + "0": "正在准备上传", + "1": "正在读取 Google 元数据文件", + "2": "文件元数据提取状态:已完成 {{uploadCounter.finished, number}} / 共 {{uploadCounter.total, number}}", + "3": "文件备份状态:已完成 {{uploadCounter.finished, number}} / 共 {{uploadCounter.total, number}}", + "4": "正在取消剩余的上传内容", + "5": "备份完成" + }, + "FILE_NOT_UPLOADED_LIST": "以下文件未上传", + "SUBSCRIPTION_EXPIRED": "您的订阅已过期", + "SUBSCRIPTION_EXPIRED_MESSAGE": "您的订阅已过期,请 <a>续期</a>", + "STORAGE_QUOTA_EXCEEDED": "已超出存储限制", + "INITIAL_LOAD_DELAY_WARNING": "第一次加载可能需要一些时间", + "USER_DOES_NOT_EXIST": "抱歉,找不到该电子邮件的用户", + "NO_ACCOUNT": "没有账号", + "ACCOUNT_EXISTS": "已有账户", + "CREATE": "创建", + "DOWNLOAD": "下载", + "DOWNLOAD_OPTION": "下载 (D)", + "DOWNLOAD_FAVORITES": "下载收藏", + "DOWNLOAD_UNCATEGORIZED": "下载未分类的", + "DOWNLOAD_HIDDEN_ITEMS": "下载隐藏项目", + "COPY_OPTION": "复制为 PNG (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "切换至全屏 (F)", + "ZOOM_IN_OUT": "放大/缩小", + "PREVIOUS": "上一个 (←)", + "NEXT": "下一个 (→)", + "TITLE_PHOTOS": "Ente 照片", + "TITLE_ALBUMS": "Ente 照片", + "TITLE_AUTH": "Ente 验证器", + "UPLOAD_FIRST_PHOTO": "上传您的第一张照片", + "IMPORT_YOUR_FOLDERS": "导入您的文件夹", + "UPLOAD_DROPZONE_MESSAGE": "拖放以备份您的文件", + "WATCH_FOLDER_DROPZONE_MESSAGE": "拖放以添加观看的文件夹", + "TRASH_FILES_TITLE": "要删除文件吗?", + "TRASH_FILE_TITLE": "要删除文件吗?", + "DELETE_FILES_TITLE": "要立即删除吗?", + "DELETE_FILES_MESSAGE": "所选文件将从您的账户中永久删除。", + "DELETE": "删除", + "DELETE_OPTION": "删除(DEL)", + "FAVORITE_OPTION": "收藏 (L)", + "UNFAVORITE_OPTION": "取消收藏 (L)", + "MULTI_FOLDER_UPLOAD": "检测到多个文件夹", + "UPLOAD_STRATEGY_CHOICE": "你想要上传他们到", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "单个相册", + "OR": "或者", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "独立相册", + "SESSION_EXPIRED_MESSAGE": "您的会话已过期,请重新登录以继续", + "SESSION_EXPIRED": "会话已过期", + "PASSWORD_GENERATION_FAILED": "您的浏览器无法生成一个符合ente加密标准的强密钥,请尝试使用移动应用程序或其他浏览器", + "CHANGE_PASSWORD": "修改密码", + "GO_BACK": "返回", + "RECOVERY_KEY": "恢复密钥", + "SAVE_LATER": "稍后再做", + "SAVE": "保存密钥", + "RECOVERY_KEY_DESCRIPTION": "如果您忘记了密码,恢复数据的唯一方法就是使用此密钥。", + "RECOVER_KEY_GENERATION_FAILED": "无法生成恢复代码,请重试", + "KEY_NOT_STORED_DISCLAIMER": "我们不存储此密钥,因此请将其保存在安全的地方", + "FORGOT_PASSWORD": "忘记密码", + "RECOVER_ACCOUNT": "恢复账户", + "RECOVERY_KEY_HINT": "恢复密钥", + "RECOVER": "恢复", + "NO_RECOVERY_KEY": "没有恢复密钥?", + "INCORRECT_RECOVERY_KEY": "不正确的恢复密钥", + "SORRY": "抱歉", + "NO_RECOVERY_KEY_MESSAGE": "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "请用您注册ente账户的电子邮箱发一封邮件给 <a>{{emailID}}</a>", + "CONTACT_SUPPORT": "联系支持", + "REQUEST_FEATURE": "功能建议", + "SUPPORT": "支持", + "CONFIRM": "确认", + "CANCEL": "取消", + "LOGOUT": "退出登录", + "DELETE_ACCOUNT": "删除账户", + "DELETE_ACCOUNT_MESSAGE": "<p>请从您注册的电子邮件地址发送一封电子邮件到 <a>{{emailID}}</a></p><p>。您的请求将在72小时内处理。</p>", + "LOGOUT_MESSAGE": "你确定要退出登录吗?", + "CHANGE_EMAIL": "更换邮箱", + "OK": "确定", + "SUCCESS": "成功", + "ERROR": "错误", + "MESSAGE": "消息", + "INSTALL_MOBILE_APP": "安装我们的 <a>Android</a> 或 <b>iOS</b> 应用程序来自动备份您的所有照片", + "DOWNLOAD_APP_MESSAGE": "抱歉,目前只有我们的桌面应用程序支持此操作", + "DOWNLOAD_APP": "下载桌面应用程序", + "EXPORT": "导出数据", + "SUBSCRIPTION": "订阅", + "SUBSCRIBE": "订阅", + "MANAGEMENT_PORTAL": "管理付款方式", + "MANAGE_FAMILY_PORTAL": "管理家庭", + "LEAVE_FAMILY_PLAN": "离开家庭计划", + "LEAVE": "离开", + "LEAVE_FAMILY_CONFIRM": "您确定要离开家庭计划吗?", + "CHOOSE_PLAN": "选择您的计划", + "MANAGE_PLAN": "管理您的订阅", + "ACTIVE": "已激活", + "OFFLINE_MSG": "您处于离线状态,正在显示已缓存的回忆", + "FREE_SUBSCRIPTION_INFO": "您使用的是将于{{date, dateTime}} 过期的<strong>免费</strong>计划", + "FAMILY_SUBSCRIPTION_INFO": "您正在使用由 管理的家庭计划", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "于 {{date, dateTime}} 续费", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "结束于 {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "您的订阅将于 {{date, dateTime}} 取消", + "ADD_ON_AVAILABLE_TILL": "您的 {{storage, string}} 插件有效期至 {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "您已超过您的存储配额,请 <a>升级</a>", + "SUBSCRIPTION_PURCHASE_SUCCESS": "<p>我们已经收到您的付款</p><p>您的订阅有效期至 <strong>{{date, dateTime}}</strong></p>", + "SUBSCRIPTION_PURCHASE_CANCELLED": "您的购买已取消,如果您想订阅,请重试", + "SUBSCRIPTION_PURCHASE_FAILED": "订阅购买失败,请重试", + "SUBSCRIPTION_UPDATE_FAILED": "订阅更新失败,请重试", + "UPDATE_PAYMENT_METHOD_MESSAGE": "很抱歉,我们尝试从您的卡中扣款时支付失败,请更新您的付款方式并重试", + "STRIPE_AUTHENTICATION_FAILED": "我们无法验证您的付款方式。请选择不同的付款方式并重试", + "UPDATE_PAYMENT_METHOD": "更新付款方式", + "MONTHLY": "每月", + "YEARLY": "每年", + "UPDATE_SUBSCRIPTION_MESSAGE": "您确定要更改您的计划吗?", + "UPDATE_SUBSCRIPTION": "更改计划", + "CANCEL_SUBSCRIPTION": "取消订阅", + "CANCEL_SUBSCRIPTION_MESSAGE": "<p>您的所有数据将在此计费期结束时从我们的服务器中删除。</p><p>您确定要取消您的订阅吗?</p>", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>您确定要取消订阅吗?</p>", + "SUBSCRIPTION_CANCEL_FAILED": "取消订阅失败", + "SUBSCRIPTION_CANCEL_SUCCESS": "订阅成功取消", + "REACTIVATE_SUBSCRIPTION": "重新激活订阅", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "重新激活后,您将在 {{date, dateTime}} 前支付费用", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "订阅已成功激活 ", + "SUBSCRIPTION_ACTIVATE_FAILED": "无法重新激活订阅续费", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "非常感谢您", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "取消手机订阅", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "请从手机应用取消您的订阅以激活这里的订阅", + "MAIL_TO_MANAGE_SUBSCRIPTION": "请联系我们 <a>{{emailID}}</a> 来管理您的订阅", + "RENAME": "重命名", + "RENAME_FILE": "重命名文件", + "RENAME_COLLECTION": "重命名相册", + "DELETE_COLLECTION_TITLE": "要删除相册吗?", + "DELETE_COLLECTION": "删除相册", + "DELETE_COLLECTION_MESSAGE": "也删除此相册中存在的照片(和视频),从 <a>他们所加入的所有</a> 个其他相册?", + "DELETE_PHOTOS": "删除照片", + "KEEP_PHOTOS": "保留照片", + "SHARE": "分享", + "SHARE_COLLECTION": "分享相册", + "SHAREES": "已分享给", + "SHARE_WITH_SELF": "哎呀,您不能与自己分享", + "ALREADY_SHARED": "哎呀,您已经和 {{email}} 分享了", + "SHARING_BAD_REQUEST_ERROR": "不允许分享相册", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "免费账户禁用共享", + "DOWNLOAD_COLLECTION": "下载相册", + "DOWNLOAD_COLLECTION_MESSAGE": "<p>您确定要下载完整相册吗?</p><p>所有文件都将按顺序排队进行下载</p>", + "CREATE_ALBUM_FAILED": "相册创建失败,请重试", + "SEARCH": "搜索", + "SEARCH_RESULTS": "搜索结果", + "NO_RESULTS": "未找到任何结果", + "SEARCH_HINT": "搜索相册、日期...", + "SEARCH_TYPE": { + "COLLECTION": "相册", + "LOCATION": "地理位置", + "CITY": "位置", + "DATE": "日期", + "FILE_NAME": "文件名", + "THING": "内容", + "FILE_CAPTION": "说明", + "FILE_TYPE": "文件类型", + "CLIP": "魔法" + }, + "photos_count_zero": "没有回忆", + "photos_count_one": "1个回忆", + "photos_count_other": "{{count, number}} 个回忆", + "TERMS_AND_CONDITIONS": "我同意 <a>条款</a> 和 <b>隐私政策</b>", + "ADD_TO_COLLECTION": "添加到相册", + "SELECTED": "已选", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "此视频无法在您的浏览器中播放", + "PEOPLE": "人物", + "INDEXING_SCHEDULED": "索引已安排...", + "ANALYZING_PHOTOS": "分析 {{indexStatus.nTotalFiles}} 的新照片{{indexStatus.nSyncedFiles}} 已完成)...", + "INDEXING_PEOPLE": "正在为 {{indexStatus.nSyncedFiles}} 张照片中的人物建立索引...", + "INDEXING_DONE": "已索引 {{indexStatus.nSyncedFiles}} 张照片", + "UNIDENTIFIED_FACES": "身份不明的面孔", + "OBJECTS": "对象", + "TEXT": "文本", + "INFO": "图片信息 ", + "INFO_OPTION": "图片信息 (I)", + "FILE_NAME": "文件名", + "CAPTION_PLACEHOLDER": "添加说明", + "LOCATION": "地理位置", + "SHOW_ON_MAP": "在 OpenStreetMap 上查看", + "MAP": "地图", + "MAP_SETTINGS": "地图设置", + "ENABLE_MAPS": "要启用地图吗?", + "ENABLE_MAP": "启用地图", + "DISABLE_MAPS": "要禁用地图吗?", + "ENABLE_MAP_DESCRIPTION": "<p>这将在世界地图上显示您的照片。</p> <p>该地图由 <a>OpenStreetMap</a> 托管,并且您照片的确切位置永远不会共享。</p><p>您可以随时从“设置”中禁用此功能。</p>", + "DISABLE_MAP_DESCRIPTION": "<p>这将禁止在世界地图上显示您的照片。</p> <p>您可以随时从“设置”中启用此功能。</p>", + "DISABLE_MAP": "禁用地图", + "DETAILS": "详情", + "VIEW_EXIF": "查看所有 EXIF 数据", + "NO_EXIF": "无 EXIF 数据", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "双因素", + "TWO_FACTOR_AUTHENTICATION": "双因素认证", + "TWO_FACTOR_QR_INSTRUCTION": "使用您最喜欢的身份验证器应用程序(2FA)扫描下面的二维码", + "ENTER_CODE_MANUALLY": "请手动输入代码", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "请在您最喜欢的验证器应用中输入此代码", + "SCAN_QR_CODE": "改为扫描二维码", + "ENABLE_TWO_FACTOR": "启用双因素认证", + "ENABLE": "启用", + "LOST_DEVICE": "丢失了双因素认证设备", + "INCORRECT_CODE": "代码错误", + "TWO_FACTOR_INFO": "登录您的账户不仅需要您的电子邮件和密码,还需要额外的安全层", + "DISABLE_TWO_FACTOR_LABEL": "禁用双因素认证", + "UPDATE_TWO_FACTOR_LABEL": "更新您的身份验证器设备", + "DISABLE": "禁用", + "RECONFIGURE": "重新配置", + "UPDATE_TWO_FACTOR": "更新双因素认证", + "UPDATE_TWO_FACTOR_MESSAGE": "向前继续将使之前配置的任何身份验证器无效", + "UPDATE": "更新", + "DISABLE_TWO_FACTOR": "禁用双因素认证", + "DISABLE_TWO_FACTOR_MESSAGE": "您确定要禁用您的双因素认证吗?", + "TWO_FACTOR_DISABLE_FAILED": "禁用双因素认证失败,请再试一次", + "EXPORT_DATA": "导出数据", + "SELECT_FOLDER": "选择文件夹", + "DESTINATION": "目标位置", + "START": "开始", + "LAST_EXPORT_TIME": "最后一次导出时间", + "EXPORT_AGAIN": "重新同步", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "无法访问本地存储", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "您的浏览器或插件阻止 ente 将数据保存到本地存储。 请在切换浏览模式后再尝试加载此页面。", + "SEND_OTT": "发送 OTP", + "EMAIl_ALREADY_OWNED": "电子邮箱已被注册", + "ETAGS_BLOCKED": "<p>由于您的浏览器配置,我们无法上传以下文件。</p><p>请禁用任何可能阻止ente 使用 <code>eTags</code> 上传大文件的附加组件, 或者使用我们的 <a>桌面应用程序</a> 获取更可靠的导入体验。</p>", + "SKIPPED_VIDEOS_INFO": "<p>目前,我们不支持在公共链接内添加视频。</p><p>若要分享视频,请 <a>注册</a> 并通过电子邮件与预定收件人分享。</p>", + "LIVE_PHOTOS_DETECTED": "Live Photos 中的照片和视频文件已合并为一个文件", + "RETRY_FAILED": "重试上传失败的文件", + "FAILED_UPLOADS": "上传失败 ", + "SKIPPED_FILES": "已忽略的上传内容", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "缩略图生成失败", + "UNSUPPORTED_FILES": "不支持的文件", + "SUCCESSFUL_UPLOADS": "上传成功", + "SKIPPED_INFO": "跳过这些,因为在同一相册中有具有匹配名称的文件", + "UNSUPPORTED_INFO": "ente 尚不支持这些文件格式", + "BLOCKED_UPLOADS": "已阻止上传", + "SKIPPED_VIDEOS": "已跳过的视频", + "INPROGRESS_METADATA_EXTRACTION": "进行中", + "INPROGRESS_UPLOADS": "上传进行中", + "TOO_LARGE_UPLOADS": "大文件", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "存储空间不足", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "这些文件没有上传,因为它们超过了您的存储计划的最大大小限制", + "TOO_LARGE_INFO": "这些文件没有上传,因为它们超过了我们的最大文件大小限制", + "THUMBNAIL_GENERATION_FAILED_INFO": "这些文件已上传,但遗憾的是,我们无法为它们生成缩略图。", + "UPLOAD_TO_COLLECTION": "上传至相册", + "UNCATEGORIZED": "未分类的", + "ARCHIVE": "存档", + "FAVORITES": "收藏", + "ARCHIVE_COLLECTION": "存档相册", + "ARCHIVE_SECTION_NAME": "存档", + "ALL_SECTION_NAME": "全部", + "MOVE_TO_COLLECTION": "移动到相册", + "UNARCHIVE": "取消存档", + "UNARCHIVE_COLLECTION": "取消存档相册", + "HIDE_COLLECTION": "隐藏相册", + "UNHIDE_COLLECTION": "取消隐藏相册", + "MOVE": "移动", + "ADD": "添加", + "REMOVE": "移除", + "YES_REMOVE": "是,移除", + "REMOVE_FROM_COLLECTION": "从相册中移除", + "TRASH": "回收站", + "MOVE_TO_TRASH": "移动到回收站", + "TRASH_FILES_MESSAGE": "选中的文件将从所有相册中删除并移动到回收站。", + "TRASH_FILE_MESSAGE": "该文件将从所有相册中删除并移动到回收站。", + "DELETE_PERMANENTLY": "永久删除", + "RESTORE": "恢复", + "RESTORE_TO_COLLECTION": "恢复到相册", + "EMPTY_TRASH": "清空回收站", + "EMPTY_TRASH_TITLE": "要清空回收站吗?", + "EMPTY_TRASH_MESSAGE": "这些文件将从您的 ente 账户中永久删除。", + "LEAVE_SHARED_ALBUM": "是,离开", + "LEAVE_ALBUM": "离开相册", + "LEAVE_SHARED_ALBUM_TITLE": "要离开共享相册吗?", + "LEAVE_SHARED_ALBUM_MESSAGE": "您将离开相册,它将不再对您可见。", + "NOT_FILE_OWNER": "您不能删除共享相册中的文件", + "CONFIRM_SELF_REMOVE_MESSAGE": "所选项目将从该相册中删除。 仅在此相册中的项目将移至未分类。", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "您要删除的某些项目是由其他人添加的,您将无法访问它们。", + "SORT_BY_CREATION_TIME_ASCENDING": "最早的", + "SORT_BY_UPDATION_TIME_DESCENDING": "最后更新", + "SORT_BY_NAME": "名称", + "COMPRESS_THUMBNAILS": "压缩缩略图", + "THUMBNAIL_REPLACED": "缩略图已压缩", + "FIX_THUMBNAIL": "压缩", + "FIX_THUMBNAIL_LATER": "稍后压缩", + "REPLACE_THUMBNAIL_NOT_STARTED": "您的一些视频缩略图可以被压缩以节省空间,您想要ente 压缩它们吗?", + "REPLACE_THUMBNAIL_COMPLETED": "已成功压缩所有缩略图", + "REPLACE_THUMBNAIL_NOOP": "您没有可以进一步压缩的缩略图", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "无法压缩您的一些缩略图,请重试", + "FIX_CREATION_TIME": "固定时间", + "FIX_CREATION_TIME_IN_PROGRESS": "正在固定时间", + "CREATION_TIME_UPDATED": "文件时间已更新", + "UPDATE_CREATION_TIME_NOT_STARTED": "选择您想要使用的选项", + "UPDATE_CREATION_TIME_COMPLETED": "已成功更新所有文件", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "部分文件的文件时间更新失败,请重试", + "CAPTION_CHARACTER_LIMIT": "5000个字符上限", + "DATE_TIME_ORIGINAL": "EXIF:日期 时间 原始文件", + "DATE_TIME_DIGITIZED": "EXIF:日期 时间 数字化", + "METADATA_DATE": "EXIF:元数据日期", + "CUSTOM_TIME": "自定义时间", + "REOPEN_PLAN_SELECTOR_MODAL": "重新启动计划", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "未能打开计划", + "INSTALL": "安装", + "SHARING_DETAILS": "共享的详细信息", + "MODIFY_SHARING": "更改共享", + "ADD_COLLABORATORS": "添加协作者", + "ADD_NEW_EMAIL": "添加新的电子邮件", + "shared_with_people_zero": "与特定人员分享", + "shared_with_people_one": "已与1个人共享", + "shared_with_people_other": "已与 {count, number} 个人共享", + "participants_zero": "暂无参与者", + "participants_one": "1 名参与者", + "participants_other": "{{count, number}} 名参与者", + "ADD_VIEWERS": "添加查看者", + "PARTICIPANTS": "参与者", + "CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} 将无法向相册添加更多照片</p> <p>他们仍然可以删除他们添加的照片</p>", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} 将能够将照片添加到相册", + "CONVERT_TO_VIEWER": "是的,转换为查看者", + "CONVERT_TO_COLLABORATOR": "是,转换为协作者", + "CHANGE_PERMISSION": "要修改权限吗?", + "REMOVE_PARTICIPANT": "要移除吗?", + "CONFIRM_REMOVE": "是,移除", + "MANAGE": "管理", + "ADDED_AS": "已添加为", + "COLLABORATOR_RIGHTS": "协作者可以将照片和视频添加到共享相册中", + "REMOVE_PARTICIPANT_HEAD": "移除参与者", + "OWNER": "所有者", + "COLLABORATORS": "协作者", + "ADD_MORE": "添加更多", + "VIEWERS": "查看者", + "OR_ADD_EXISTING": "或选择一个现有的", + "REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} 将从相册中删除</p> <p>他们添加的所有照片也将从相册中删除</p>", + "NOT_FOUND": "404 - 未找到", + "LINK_EXPIRED": "链接已过期", + "LINK_EXPIRED_MESSAGE": "此链接已过期或已被禁用!", + "MANAGE_LINK": "管理链接", + "LINK_TOO_MANY_REQUESTS": "这个相册太受欢迎,我们无法处理!", + "FILE_DOWNLOAD": "允许下载", + "LINK_PASSWORD_LOCK": "密码锁", + "PUBLIC_COLLECT": "允许添加照片", + "LINK_DEVICE_LIMIT": "设备限制", + "NO_DEVICE_LIMIT": "无", + "LINK_EXPIRY": "链接过期", + "NEVER": "永不", + "DISABLE_FILE_DOWNLOAD": "禁止下载", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>您确定要禁用文件下载按钮吗?</p><p>观看者仍然可以使用外部工具进行屏幕截图或保存您的照片副本。</p>", + "MALICIOUS_CONTENT": "哈哈哈急急急", + "COPYRIGHT": "不不不急急急就是", + "SHARED_USING": "分享方式 ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "使用代码 <strong>{{referralCode}}</strong> 获得 10 GB 免费空间", + "LIVE": "LIVE", + "DISABLE_PASSWORD": "禁用密码锁", + "DISABLE_PASSWORD_MESSAGE": "您确定要禁用密码锁吗?", + "PASSWORD_LOCK": "密码锁", + "LOCK": "锁定", + "DOWNLOAD_UPLOAD_LOGS": "调试日志", + "UPLOAD_FILES": "文件", + "UPLOAD_DIRS": "文件夹", + "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", + "DEDUPLICATE_FILES": "删除重复文件", + "AUTHENTICATOR_SECTION": "身份验证器", + "NO_DUPLICATES_FOUND": "您没有可以清除的重复文件", + "CLUB_BY_CAPTURE_TIME": "按抓取时间断开", + "FILES": "文件", + "EACH": "每个", + "DEDUPLICATE_BASED_ON_SIZE": "以下文件根据大小进行了合并,请检查并删除您认为重复的项目", + "STOP_ALL_UPLOADS_MESSAGE": "您确定要停止所有正在进行的上传吗?", + "STOP_UPLOADS_HEADER": "要停止上传吗?", + "YES_STOP_UPLOADS": "是的,停止上传", + "STOP_DOWNLOADS_HEADER": "要停止下载吗?", + "YES_STOP_DOWNLOADS": "是,停止下载", + "STOP_ALL_DOWNLOADS_MESSAGE": "您确定要停止所有正在进行的下载?", + "albums_one": "1个相册", + "albums_other": "{{count, number}} 个相册", + "ALL_ALBUMS": "所有相册", + "ALBUMS": "相册", + "ALL_HIDDEN_ALBUMS": "所有隐藏的相册", + "HIDDEN_ALBUMS": "隐藏的相册", + "HIDDEN_ITEMS": "隐藏的项目", + "HIDDEN_ITEMS_SECTION_NAME": "隐藏的项目", + "ENTER_TWO_FACTOR_OTP": "请输入您从身份验证应用上获得的6位数代码", + "CREATE_ACCOUNT": "创建账户", + "COPIED": "已复制", + "CANVAS_BLOCKED_TITLE": "无法生成缩略图", + "CANVAS_BLOCKED_MESSAGE": "<p>看起来您的浏览器已禁用了需要为您的照片生成缩略图的canvas访问权限 </p> <p> 请允许访问您浏览器的canvas, 或使用我们的桌面应用程序</p>", + "WATCH_FOLDERS": "观看文件夹", + "UPGRADE_NOW": "立即升级", + "RENEW_NOW": "立即续费", + "STORAGE": "存储空间", + "USED": "已使用", + "YOU": "您", + "FAMILY": "家庭", + "FREE": "空闲", + "OF": "/", + "WATCHED_FOLDERS": "观看文件夹", + "NO_FOLDERS_ADDED": "尚未添加任何文件夹!", + "FOLDERS_AUTOMATICALLY_MONITORED": "您在此处添加的文件夹将自动监控", + "UPLOAD_NEW_FILES_TO_ENTE": "上传新文件至 ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "从ente 移除已删除的文件", + "ADD_FOLDER": "添加文件夹", + "STOP_WATCHING": "停止监控", + "STOP_WATCHING_FOLDER": "要停止监控文件夹?", + "STOP_WATCHING_DIALOG_MESSAGE": "您现有的文件不会被删除,但 ente 将停止自动更新链接的 ente 相册在此文件夹中的更改。", + "YES_STOP": "是的,停止", + "MONTH_SHORT": "月", + "YEAR": "年", + "FAMILY_PLAN": "家庭计划", + "DOWNLOAD_LOGS": "下载日志", + "DOWNLOAD_LOGS_MESSAGE": "<p>这将下载调试日志,您可以发送电子邮件给我们来帮助调试您的问题。</p><p> 请注意文件名将被包含,以帮助跟踪特定文件中的问题。 </p>", + "CHANGE_FOLDER": "更改文件夹", + "TWO_MONTHS_FREE": "在年度计划上免费获得 2 个月", + "GB": "GB", + "POPULAR": "流行的", + "FREE_PLAN_OPTION_LABEL": "继续免费试用", + "FREE_PLAN_DESCRIPTION": "1 GB 1年", + "CURRENT_USAGE": "当前使用量是 <strong>{{usage}}</strong>", + "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录ente,或下载ente移动/桌面应用程序。", + "DRAG_AND_DROP_HINT": "或者拖动并拖动到 ente 窗口", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "您上传的数据将被安排删除,您的账户将被永久删除。<br/><br/>此操作不可逆。", + "AUTHENTICATE": "身份认证", + "UPLOADED_TO_SINGLE_COLLECTION": "已上传到单个收藏", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "已上传到单独收藏", + "NEVERMIND": "没关系", + "UPDATE_AVAILABLE": "有可用的更新", + "UPDATE_INSTALLABLE_MESSAGE": "新版本的 ente 已准备好安装。", + "INSTALL_NOW": "立即安装", + "INSTALL_ON_NEXT_LAUNCH": "在下次启动时安装", + "UPDATE_AVAILABLE_MESSAGE": "新版本的 ente 已发布,但无法自动下载和安装。", + "DOWNLOAD_AND_INSTALL": "下载并安装", + "IGNORE_THIS_VERSION": "忽略该版本", + "TODAY": "今天", + "YESTERDAY": "昨天", + "NAME_PLACEHOLDER": "名称...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "无法从文件/文件夹组合中创建相册", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>你已拖放了文件和文件夹的组合。</p><p>选择创建单独相册的选项时,请只提供文件或只提供文件夹</p>", + "CHOSE_THEME": "选择主题", + "ML_SEARCH": "ML 搜索 (测试版)", + "ENABLE_ML_SEARCH_DESCRIPTION": "<p>这将启用设备上的机器学习和面部搜索,这将开始分析您上传的本地照片。</p><p>在登录或启用此功能后第一次运行时,它将下载本地设备上的所有图像来分析。 所以请只在您可以使用带宽和本地处理您的照片库中的所有图像时启用此功能。</p><p>如果这是您首次启用此功能,我们也会请求您处理面部数据的许可。</p>", + "ML_MORE_DETAILS": "更多详情", + "ENABLE_FACE_SEARCH": "启用面部搜索", + "ENABLE_FACE_SEARCH_TITLE": "要启用面部搜索吗?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "<p>如果您启用面部搜索,ente 将从照片中提取脸部几何形状。 这将发生在您的设备上,任何生成的生物测定数据都将是端到端加密的。<p/><p><a>请单击此处以在我们的隐私政策中了解有关此功能的更多详细信息</a></p>", + "DISABLE_BETA": "禁用beta", + "DISABLE_FACE_SEARCH": "禁用面部搜索", + "DISABLE_FACE_SEARCH_TITLE": "要禁用面部搜索吗?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente 将停止处理面部的几何形状, 并将禁用 ML 搜索 (测试版)</p><p>如果您愿意,您可以重新启用面部搜索,因此该操作是安全的。</p>", + "ADVANCED": "高级设置", + "FACE_SEARCH_CONFIRMATION": "我理解,并希望允许ente处理面部几何形状", + "LABS": "实验室", + "YOURS": "你的", + "PASSPHRASE_STRENGTH_WEAK": "密码强度:较弱", + "PASSPHRASE_STRENGTH_MODERATE": "密码强度:中度", + "PASSPHRASE_STRENGTH_STRONG": "密码强度:强", + "PREFERENCES": "首选项", + "LANGUAGE": "语言", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "无效的导出目录", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>您选择的导出目录不存在。</p><p> 请选择一个有效的目录。</p>", + "SUBSCRIPTION_VERIFICATION_ERROR": "订阅验证失败", + "STORAGE_UNITS": { + "B": "B", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB" + }, + "AFTER_TIME": { + "HOUR": "1小时后", + "DAY": "一天后", + "WEEK": "一周后", + "MONTH": "一个月后", + "YEAR": "一年后" + }, + "COPY_LINK": "复制链接", + "DONE": "已完成", + "LINK_SHARE_TITLE": "或共享一个链接", + "REMOVE_LINK": "移除链接", + "CREATE_PUBLIC_SHARING": "创建公开链接", + "PUBLIC_LINK_CREATED": "公开链接已创建", + "PUBLIC_LINK_ENABLED": "公开链接已启用", + "COLLECT_PHOTOS": "收集照片", + "PUBLIC_COLLECT_SUBTEXT": "允许具有链接的人也将照片添加到共享相册。", + "STOP_EXPORT": "停止", + "EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> 个文件已导出", + "MIGRATING_EXPORT": "准备中...", + "RENAMING_COLLECTION_FOLDERS": "正在重命名相册文件夹...", + "TRASHING_DELETED_FILES": "正在回收删除的文件...", + "TRASHING_DELETED_COLLECTIONS": "正在回收已删除的相册...", + "EXPORT_NOTIFICATION": { + "START": "导出已开始", + "IN_PROGRESS": "导出已在进行中", + "FINISH": "导出完成", + "UP_TO_DATE": "没有新文件可导出" + }, + "CONTINUOUS_EXPORT": "持续同步", + "TOTAL_ITEMS": "项目总计", + "PENDING_ITEMS": "待处理的项目", + "EXPORT_STARTING": "导出开始...", + "DELETE_ACCOUNT_REASON_LABEL": "您删除账户的主要原因是什么?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "选择一个原因", + "DELETE_REASON": { + "MISSING_FEATURE": "找不到我想要的功能", + "BROKEN_BEHAVIOR": "该应用或某个功能不符合我认为应该做的行为", + "FOUND_ANOTHER_SERVICE": "我发现另一个产品更好用", + "NOT_LISTED": "我的原因未被列出" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "我们很抱歉看到您离开。请解释您为什么要离开来帮助我们改进。", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "反馈", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "是的,我想永久删除此账户及其相关数据", + "CONFIRM_DELETE_ACCOUNT": "确认删除账户", + "FEEDBACK_REQUIRED": "请帮助我们了解这个信息", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "其他服务做得更好?", + "RECOVER_TWO_FACTOR": "恢复双因素认证", + "at": "在", + "AUTH_NEXT": "下一个", + "AUTH_DOWNLOAD_MOBILE_APP": "下载我们的移动应用程序来管理您的密钥", + "HIDDEN": "已隐藏", + "HIDE": "隐藏", + "UNHIDE": "取消隐藏", + "UNHIDE_TO_COLLECTION": "取消隐藏到相册", + "SORT_BY": "排序方式", + "NEWEST_FIRST": "最新在前", + "OLDEST_FIRST": "最旧在前", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "无法预览此文件。点击这里下载原始文件。", + "SELECT_COLLECTION": "选择相册", + "PIN_ALBUM": "置顶相册", + "UNPIN_ALBUM": "取消置顶相册", + "DOWNLOAD_COMPLETE": "下载完成", + "DOWNLOADING_COLLECTION": "正在下载 {{name}}", + "DOWNLOAD_FAILED": "下载失败", + "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} 个文件", + "CHRISTMAS": "圣诞", + "CHRISTMAS_EVE": "平安夜", + "NEW_YEAR": "新年", + "NEW_YEAR_EVE": "除夕", + "IMAGE": "图像", + "VIDEO": "视频", + "LIVE_PHOTO": "实况照片", + "CONVERT": "转换", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "您确定要关闭编辑器吗?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "下载已编辑的图片或将副本保存到 ente 以保留您的更改。", + "BRIGHTNESS": "亮度", + "CONTRAST": "对比度", + "SATURATION": "饱和度", + "BLUR": "模糊", + "INVERT_COLORS": "反相颜色", + "ASPECT_RATIO": "长宽比", + "SQUARE": "面积", + "ROTATE_LEFT": "向左旋转", + "ROTATE_RIGHT": "向右旋转", + "FLIP_VERTICALLY": "垂直翻转", + "FLIP_HORIZONTALLY": "水平翻转", + "DOWNLOAD_EDITED": "下载已编辑图片", + "SAVE_A_COPY_TO_ENTE": "保存副本到 ente", + "RESTORE_ORIGINAL": "复原", + "TRANSFORM": "转换", + "COLORS": "颜色", + "FLIP": "上下翻转", + "ROTATION": "回转", + "RESET": "重设", + "PHOTO_EDITOR": "照片编辑器", + "FASTER_UPLOAD": "更快上传", + "FASTER_UPLOAD_DESCRIPTION": "通过附近的服务器路由上传", + "MAGIC_SEARCH_STATUS": "魔法搜索状态", + "INDEXED_ITEMS": "索引项目", + "CAST_ALBUM_TO_TV": "在电视上播放相册", + "ENTER_CAST_PIN_CODE": "输入您在下面的电视上看到的代码来配对此设备。", + "PAIR_DEVICE_TO_TV": "配对设备", + "TV_NOT_FOUND": "未找到电视。您输入的 PIN 码正确吗?", + "AUTO_CAST_PAIR": "自动配对", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "自动配对需要连接到 Google 服务器,且仅适用于支持 Chromecast 的设备。Google 不会接收敏感数据,例如您的照片。", + "PAIR_WITH_PIN": "用 PIN 配对", + "CHOOSE_DEVICE_FROM_BROWSER": "从浏览器弹出窗口中选择兼容 Cast 的设备。", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "用 PIN 配对适用于任何大屏幕设备,您可以在这些设备上播放您的相册。", + "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 cast.ente.io 。", + "CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。", + "CACHE_DIRECTORY": "缓存文件夹", + "FREEHAND": "手画", + "APPLY_CROP": "应用裁剪", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。", + "PASSKEYS": "通行密钥", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" +} diff --git a/web/apps/cast/src/components/LargeType.tsx b/web/apps/cast/src/components/LargeType.tsx index bb0728699..ecf7a201b 100644 --- a/web/apps/cast/src/components/LargeType.tsx +++ b/web/apps/cast/src/components/LargeType.tsx @@ -1,3 +1,5 @@ +import { styled } from "@mui/material"; + const colourPool = [ "#87CEFA", // Light Blue "#90EE90", // Light Green @@ -23,44 +25,41 @@ const colourPool = [ export default function LargeType({ chars }: { chars: string[] }) { return ( - <table - style={{ - fontSize: "4rem", - fontWeight: "bold", - fontFamily: "monospace", - display: "flex", - position: "relative", - }} - > + <Container style={{}}> {chars.map((char, i) => ( - <tr + <span key={i} style={{ - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "0.5rem", // alternating background backgroundColor: i % 2 === 0 ? "#2e2e2e" : "#5e5e5e", + // varying colors + color: colourPool[i % colourPool.length], }} > - <span - style={{ - color: colourPool[i % colourPool.length], - lineHeight: 1.2, - }} - > - {char} - </span> - <span - style={{ - fontSize: "1rem", - }} - > - {i + 1} - </span> - </tr> + {char} + </span> ))} - </table> + </Container> ); } + +const Container = styled("div")` + font-size: 4rem; + font-weight: bold; + font-family: monospace; + + line-height: 1.2; + + /* + * - We want them to be spans so that when the text is copy pasted, there + * is no extra whitespace inserted. + * + * - But we also want them to have a block level padding. + * + * To achieve both these goals, make them inline-blocks + */ + span { + display: inline-block; + padding: 0.5rem; + } +`; diff --git a/web/apps/cast/src/components/PhotoAuditorium.tsx b/web/apps/cast/src/components/PhotoAuditorium.tsx new file mode 100644 index 000000000..0042dfe95 --- /dev/null +++ b/web/apps/cast/src/components/PhotoAuditorium.tsx @@ -0,0 +1,95 @@ +import { SlideshowContext } from "pages/slideshow"; +import { useContext, useEffect, useState } from "react"; + +export default function PhotoAuditorium({ + url, + nextSlideUrl, +}: { + url: string; + nextSlideUrl: string; +}) { + const { showNextSlide } = useContext(SlideshowContext); + + const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false); + const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false); + const [prerenderTime, setPrerenderTime] = useState<number | null>(null); + + useEffect(() => { + let timeout: NodeJS.Timeout; + let timeout2: NodeJS.Timeout; + + if (nextSlidePrerendered) { + const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0; + const delayTime = Math.max(10000 - elapsedTime, 0); + + if (elapsedTime >= 10000) { + setShowPreloadedNextSlide(true); + } else { + timeout = setTimeout(() => { + setShowPreloadedNextSlide(true); + }, delayTime); + } + + if (showNextSlide) { + timeout2 = setTimeout(() => { + showNextSlide(); + setNextSlidePrerendered(false); + setPrerenderTime(null); + setShowPreloadedNextSlide(false); + }, delayTime); + } + } + + return () => { + if (timeout) clearTimeout(timeout); + if (timeout2) clearTimeout(timeout2); + }; + }, [nextSlidePrerendered, showNextSlide, prerenderTime]); + + return ( + <div + style={{ + width: "100vw", + height: "100vh", + backgroundImage: `url(${url})`, + backgroundSize: "cover", + backgroundPosition: "center", + backgroundRepeat: "no-repeat", + backgroundBlendMode: "multiply", + backgroundColor: "rgba(0, 0, 0, 0.5)", + }} + > + <div + style={{ + height: "100%", + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + backdropFilter: "blur(10px)", + }} + > + <img + src={url} + style={{ + maxWidth: "100%", + maxHeight: "100%", + display: showPreloadedNextSlide ? "none" : "block", + }} + /> + <img + src={nextSlideUrl} + style={{ + maxWidth: "100%", + maxHeight: "100%", + display: showPreloadedNextSlide ? "block" : "none", + }} + onLoad={() => { + setNextSlidePrerendered(true); + setPrerenderTime(Date.now()); + }} + /> + </div> + </div> + ); +} diff --git a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx b/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx index dc5a18f0b..0042dfe95 100644 --- a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx +++ b/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx @@ -20,9 +20,9 @@ export default function PhotoAuditorium({ if (nextSlidePrerendered) { const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0; - const delayTime = Math.max(5000 - elapsedTime, 0); + const delayTime = Math.max(10000 - elapsedTime, 0); - if (elapsedTime >= 5000) { + if (elapsedTime >= 10000) { setShowPreloadedNextSlide(true); } else { timeout = setTimeout(() => { diff --git a/web/apps/cast/src/components/TimerBar.tsx b/web/apps/cast/src/components/TimerBar.tsx deleted file mode 100644 index 7f4d02171..000000000 --- a/web/apps/cast/src/components/TimerBar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; - -export default function TimerBar({ percentage }: { percentage: number }) { - const okColor = "#75C157"; - const warningColor = "#FFC000"; - const lateColor = "#FF0000"; - - const [backgroundColor, setBackgroundColor] = useState(okColor); - - useEffect(() => { - if (percentage >= 40) { - setBackgroundColor(okColor); - } else if (percentage >= 20) { - setBackgroundColor(warningColor); - } else { - setBackgroundColor(lateColor); - } - }, [percentage]); - - return ( - <div - style={{ - width: `${percentage}%`, // Set the width based on the time left - height: "10px", // Same as the border thickness - backgroundColor, // The color of the moving border - transition: "width 1s linear", // Smooth transition for the width change - }} - /> - ); -} diff --git a/web/apps/cast/src/constants/apps.ts b/web/apps/cast/src/constants/apps.ts deleted file mode 100644 index f8c3f9657..000000000 --- a/web/apps/cast/src/constants/apps.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getAlbumsURL } from "@ente/shared/network/api"; -import { runningInBrowser } from "@ente/shared/platform"; -import { PAGES } from "constants/pages"; - -export enum APPS { - PHOTOS = "PHOTOS", - AUTH = "AUTH", - ALBUMS = "ALBUMS", -} - -export const ALLOWED_APP_PAGES = new Map([ - [APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]], - [ - APPS.AUTH, - [ - PAGES.ROOT, - PAGES.LOGIN, - PAGES.SIGNUP, - PAGES.VERIFY, - PAGES.CREDENTIALS, - PAGES.RECOVER, - PAGES.CHANGE_PASSWORD, - PAGES.GENERATE, - PAGES.AUTH, - PAGES.TWO_FACTOR_VERIFY, - PAGES.TWO_FACTOR_RECOVER, - ], - ], -]); - -export const CLIENT_PACKAGE_NAMES = new Map([ - [APPS.ALBUMS, "io.ente.albums.web"], - [APPS.PHOTOS, "io.ente.photos.web"], - [APPS.AUTH, "io.ente.auth.web"], -]); - -export const getAppNameAndTitle = () => { - if (!runningInBrowser()) { - return {}; - } - const currentURL = new URL(window.location.href); - const albumsURL = new URL(getAlbumsURL()); - if (currentURL.origin === albumsURL.origin) { - return { name: APPS.ALBUMS, title: "ente Photos" }; - } else { - return { name: APPS.PHOTOS, title: "ente Photos" }; - } -}; - -export const getAppTitle = () => { - return getAppNameAndTitle().title; -}; - -export const getAppName = () => { - return getAppNameAndTitle().name; -}; diff --git a/web/apps/cast/src/constants/cache.ts b/web/apps/cast/src/constants/cache.ts deleted file mode 100644 index cf88f63a2..000000000 --- a/web/apps/cast/src/constants/cache.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum CACHES { - THUMBS = "thumbs", - FACE_CROPS = "face-crops", - FILES = "files", -} diff --git a/web/apps/cast/src/constants/file.ts b/web/apps/cast/src/constants/file.ts index 46065136c..9be574638 100644 --- a/web/apps/cast/src/constants/file.ts +++ b/web/apps/cast/src/constants/file.ts @@ -1,14 +1,3 @@ -export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1); -export const MAX_EDITED_CREATION_TIME = new Date(); - -export const MAX_EDITED_FILE_NAME_LENGTH = 100; -export const MAX_CAPTION_SIZE = 5000; - -export const TYPE_HEIC = "heic"; -export const TYPE_HEIF = "heif"; -export const TYPE_JPEG = "jpeg"; -export const TYPE_JPG = "jpg"; - export enum FILE_TYPE { IMAGE, VIDEO, @@ -29,15 +18,3 @@ export const RAW_FORMATS = [ "dng", "tif", ]; -export const SUPPORTED_RAW_FORMATS = [ - "heic", - "rw2", - "tiff", - "arw", - "cr3", - "cr2", - "nef", - "psd", - "dng", - "tif", -]; diff --git a/web/apps/cast/src/constants/gallery.ts b/web/apps/cast/src/constants/gallery.ts deleted file mode 100644 index 9865d2e80..000000000 --- a/web/apps/cast/src/constants/gallery.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const GAP_BTW_TILES = 4; -export const DATE_CONTAINER_HEIGHT = 48; -export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72; -export const IMAGE_CONTAINER_MAX_HEIGHT = 180; -export const IMAGE_CONTAINER_MAX_WIDTH = 180; -export const MIN_COLUMNS = 4; -export const SPACE_BTW_DATES = 44; -export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244; - -export enum PLAN_PERIOD { - MONTH = "month", - YEAR = "year", -} - -export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes diff --git a/web/apps/cast/src/constants/pages.ts b/web/apps/cast/src/constants/pages.ts deleted file mode 100644 index af532801d..000000000 --- a/web/apps/cast/src/constants/pages.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum PAGES { - CHANGE_EMAIL = "/change-email", - CHANGE_PASSWORD = "/change-password", - CREDENTIALS = "/credentials", - GALLERY = "/gallery", - GENERATE = "/generate", - LOGIN = "/login", - RECOVER = "/recover", - SIGNUP = "/signup", - TWO_FACTOR_SETUP = "/two-factor/setup", - TWO_FACTOR_VERIFY = "/two-factor/verify", - TWO_FACTOR_RECOVER = "/two-factor/recover", - VERIFY = "/verify", - ROOT = "/", - SHARED_ALBUMS = "/shared-albums", - // ML_DEBUG = '/ml-debug', - DEDUPLICATE = "/deduplicate", - // AUTH page is used to show (auth)enticator codes - AUTH = "/auth", -} diff --git a/web/apps/cast/src/constants/upload.ts b/web/apps/cast/src/constants/upload.ts index bc6006e46..63d044fb4 100644 --- a/web/apps/cast/src/constants/upload.ts +++ b/web/apps/cast/src/constants/upload.ts @@ -1,11 +1,5 @@ -import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import { FILE_TYPE } from "constants/file"; -import { - FileTypeInfo, - ImportSuggestion, - Location, - ParsedExtractedMetadata, -} from "types/upload"; +import { FileTypeInfo } from "types/upload"; // list of format that were missed by type-detection for some files. export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [ @@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [ ]; export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"]; - -export const EXIFLESS_FORMATS = ["gif", "bmp"]; - -// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. -export const MULTIPART_PART_SIZE = 20 * 1024 * 1024; - -export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE; - -export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor( - MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE, -); - -export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); - -export const NULL_LOCATION: Location = { latitude: null, longitude: null }; - -export enum UPLOAD_STAGES { - START, - READING_GOOGLE_METADATA_FILES, - EXTRACTING_METADATA, - UPLOADING, - CANCELLING, - FINISH, -} - -export enum UPLOAD_STRATEGY { - SINGLE_COLLECTION, - COLLECTION_PER_FOLDER, -} - -export enum UPLOAD_RESULT { - FAILED, - ALREADY_UPLOADED, - UNSUPPORTED, - BLOCKED, - TOO_LARGE, - LARGER_THAN_AVAILABLE_STORAGE, - UPLOADED, - UPLOADED_WITH_STATIC_THUMBNAIL, - ADDED_SYMLINK, -} - -export enum PICKED_UPLOAD_TYPE { - FILES = "files", - FOLDERS = "folders", - ZIPS = "zips", -} - -export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB - -export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB - -export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = { - location: NULL_LOCATION, - creationTime: null, - width: null, - height: null, -}; - -export const A_SEC_IN_MICROSECONDS = 1e6; - -export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { - rootFolderName: "", - hasNestedFolders: false, - hasRootLevelFileWithFolder: false, -}; - -export const BLACK_THUMBNAIL_BASE64 = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" + - "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" + - "EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" + - "ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" + - "BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" + - "6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" + - "W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" + - "AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" + - "nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" + - "kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" + - "AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" + - "gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" + - "ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k="; diff --git a/web/apps/cast/src/constants/urls.ts b/web/apps/cast/src/constants/urls.ts deleted file mode 100644 index b5b453c31..000000000 --- a/web/apps/cast/src/constants/urls.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const ENTE_WEBSITE_LINK = "https://ente.io"; - -export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta"; - -export const FACE_SEARCH_PRIVACY_POLICY_LINK = - "https://ente.io/privacy#8-biometric-information-privacy-policy"; - -export const SUPPORT_EMAIL = "support@ente.io"; - -export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop"; - -export const FEEDBACK_EMAIL = "feedback@ente.io"; - -export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io"; - -export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions"; - -export const DESKTOP_ROADMAP_URL = - "https://github.com/ente-io/ente/discussions"; diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index a49d497de..3251b26ab 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -59,21 +59,12 @@ export default function Slideshow() { } }; - const init = async () => { - try { - const castToken = window.localStorage.getItem("castToken"); - setCastToken(castToken); - } catch (e) { - logError(e, "error during sync"); - router.push("/"); - } - }; - useEffect(() => { if (castToken) { const intervalId = setInterval(() => { syncCastFiles(castToken); - }, 5000); + }, 10000); + syncCastFiles(castToken); return () => clearInterval(intervalId); } @@ -105,7 +96,20 @@ export default function Slideshow() { const router = useRouter(); useEffect(() => { - init(); + try { + const castToken = window.localStorage.getItem("castToken"); + // Wait 2 seconds to ensure the green tick and the confirmation + // message remains visible for at least 2 seconds before we start + // the slideshow. + const timeoutId = setTimeout(() => { + setCastToken(castToken); + }, 2000); + + return () => clearTimeout(timeoutId); + } catch (e) { + logError(e, "error during sync"); + router.push("/"); + } }, []); useEffect(() => { diff --git a/web/apps/cast/src/services/InMemoryStore.ts b/web/apps/cast/src/services/InMemoryStore.ts deleted file mode 100644 index 88e77b869..000000000 --- a/web/apps/cast/src/services/InMemoryStore.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum MS_KEYS { - SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress", - REDIRECT_URL = "redirectUrl", -} - -type StoreType = Map<Partial<MS_KEYS>, any>; - -class InMemoryStore { - private store: StoreType = new Map(); - - get(key: MS_KEYS) { - return this.store.get(key); - } - - set(key: MS_KEYS, value: any) { - this.store.set(key, value); - } - - delete(key: MS_KEYS) { - this.store.delete(key); - } - - has(key: MS_KEYS) { - return this.store.has(key); - } - clear() { - this.store.clear(); - } -} - -export default new InMemoryStore(); diff --git a/web/apps/cast/src/services/cache/cacheStorageFactory.ts b/web/apps/cast/src/services/cache/cacheStorageFactory.ts deleted file mode 100644 index fd979e2f9..000000000 --- a/web/apps/cast/src/services/cache/cacheStorageFactory.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { LimitedCacheStorage } from "types/cache/index"; -// import { ElectronCacheStorage } from 'services/electron/cache'; -// import { runningInElectron, runningInWorker } from 'utils/common'; -// import { WorkerElectronCacheStorageService } from 'services/workerElectronCache/service'; - -class cacheStorageFactory { - // workerElectronCacheStorageServiceInstance: WorkerElectronCacheStorageService; - getCacheStorage(): LimitedCacheStorage { - // if (runningInElectron()) { - // if (runningInWorker()) { - // if (!this.workerElectronCacheStorageServiceInstance) { - // // this.workerElectronCacheStorageServiceInstance = - // // new WorkerElectronCacheStorageService(); - // } - // return this.workerElectronCacheStorageServiceInstance; - // } else { - // // return ElectronCacheStorage; - // } - // } else { - return transformBrowserCacheStorageToLimitedCacheStorage(caches); - // } - } -} - -export const CacheStorageFactory = new cacheStorageFactory(); - -function transformBrowserCacheStorageToLimitedCacheStorage( - caches: CacheStorage, -): LimitedCacheStorage { - return { - async open(cacheName) { - const cache = await caches.open(cacheName); - return { - match: cache.match.bind(cache), - put: cache.put.bind(cache), - delete: cache.delete.bind(cache), - }; - }, - delete: caches.delete.bind(caches), - }; -} diff --git a/web/apps/cast/src/services/cache/cacheStorageService.ts b/web/apps/cast/src/services/cache/cacheStorageService.ts deleted file mode 100644 index 391aefb55..000000000 --- a/web/apps/cast/src/services/cache/cacheStorageService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import { CacheStorageFactory } from "./cacheStorageFactory"; - -const SecurityError = "SecurityError"; -const INSECURE_OPERATION = "The operation is insecure."; -async function openCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().open(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - logError(e, "openCache failed"); - } - } -} -async function deleteCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().delete(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - logError(e, "deleteCache failed"); - } - } -} - -export const CacheStorageService = { open: openCache, delete: deleteCache }; diff --git a/web/apps/cast/src/services/castDownloadManager.ts b/web/apps/cast/src/services/castDownloadManager.ts index b56aec928..a99f0481d 100644 --- a/web/apps/cast/src/services/castDownloadManager.ts +++ b/web/apps/cast/src/services/castDownloadManager.ts @@ -1,162 +1,14 @@ -import { EnteFile } from "types/file"; -import { - createTypedObjectURL, - generateStreamFromArrayBuffer, - getRenderableFileURL, -} from "utils/file"; - import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api"; -import { logError } from "@ente/shared/sentry"; -import { CACHES } from "constants/cache"; +import { getCastFileURL } from "@ente/shared/network/api"; import { FILE_TYPE } from "constants/file"; -import { LimitedCache } from "types/cache"; +import { EnteFile } from "types/file"; import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; -import { CacheStorageService } from "./cache/cacheStorageService"; +import { generateStreamFromArrayBuffer } from "utils/file"; class CastDownloadManager { - private fileObjectURLPromise = new Map< - string, - Promise<{ original: string[]; converted: string[] }> - >(); - private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); - - private fileDownloadProgress = new Map<number, number>(); - - private progressUpdater: (value: Map<number, number>) => void; - - setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) { - this.progressUpdater = progressUpdater; - } - - private async getThumbnailCache() { - try { - const thumbnailCache = await CacheStorageService.open( - CACHES.THUMBS, - ); - return thumbnailCache; - } catch (e) { - return null; - // ignore - } - } - - public async getCachedThumbnail( - file: EnteFile, - thumbnailCache?: LimitedCache, - ) { - try { - if (!thumbnailCache) { - thumbnailCache = await this.getThumbnailCache(); - } - const cacheResp: Response = await thumbnailCache?.match( - file.id.toString(), - ); - - if (cacheResp) { - return URL.createObjectURL(await cacheResp.blob()); - } - return null; - } catch (e) { - logError(e, "failed to get cached thumbnail"); - throw e; - } - } - - public async getThumbnail(file: EnteFile, castToken: string) { - try { - if (!this.thumbnailObjectURLPromise.has(file.id)) { - const downloadPromise = async () => { - const thumbnailCache = await this.getThumbnailCache(); - const cachedThumb = await this.getCachedThumbnail( - file, - thumbnailCache, - ); - if (cachedThumb) { - return cachedThumb; - } - - const thumb = await this.downloadThumb(castToken, file); - const thumbBlob = new Blob([thumb]); - try { - await thumbnailCache?.put( - file.id.toString(), - new Response(thumbBlob), - ); - } catch (e) { - // TODO: handle storage full exception. - } - return URL.createObjectURL(thumbBlob); - }; - this.thumbnailObjectURLPromise.set(file.id, downloadPromise()); - } - - return await this.thumbnailObjectURLPromise.get(file.id); - } catch (e) { - this.thumbnailObjectURLPromise.delete(file.id); - logError(e, "get castDownloadManager preview Failed"); - throw e; - } - } - - private downloadThumb = async (castToken: string, file: EnteFile) => { - const resp = await HTTPService.get( - getCastThumbnailURL(file.id), - null, - { - "X-Cast-Access-Token": castToken, - }, - { responseType: "arraybuffer" }, - ); - if (typeof resp.data === "undefined") { - throw Error(CustomError.REQUEST_FAILED); - } - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const decrypted = await cryptoWorker.decryptThumbnail( - new Uint8Array(resp.data), - await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), - file.key, - ); - return decrypted; - }; - - getFile = async (file: EnteFile, castToken: string, forPreview = false) => { - const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; - try { - const getFilePromise = async () => { - const fileStream = await this.downloadFile(castToken, file); - const fileBlob = await new Response(fileStream).blob(); - if (forPreview) { - return await getRenderableFileURL(file, fileBlob); - } else { - const fileURL = await createTypedObjectURL( - fileBlob, - file.metadata.title, - ); - return { converted: [fileURL], original: [fileURL] }; - } - }; - - if (!this.fileObjectURLPromise.get(fileKey)) { - this.fileObjectURLPromise.set(fileKey, getFilePromise()); - } - const fileURLs = await this.fileObjectURLPromise.get(fileKey); - return fileURLs; - } catch (e) { - this.fileObjectURLPromise.delete(fileKey); - logError(e, "castDownloadManager failed to get file"); - throw e; - } - }; - - public async getCachedOriginalFile(file: EnteFile) { - return await this.fileObjectURLPromise.get(file.id.toString()); - } - async downloadFile(castToken: string, file: EnteFile) { const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const onDownloadProgress = this.trackDownloadProgress(file.id); if ( file.metadata.fileType === FILE_TYPE.IMAGE || @@ -187,9 +39,6 @@ class CastDownloadManager { }); const reader = resp.body.getReader(); - const contentLength = +resp.headers.get("Content-Length"); - let downloadedBytes = 0; - const stream = new ReadableStream({ async start(controller) { const decryptionHeader = await cryptoWorker.fromB64( @@ -208,11 +57,6 @@ class CastDownloadManager { reader.read().then(async ({ done, value }) => { // Is there more data to read? if (!done) { - downloadedBytes += value.byteLength; - onDownloadProgress({ - loaded: downloadedBytes, - total: contentLength, - }); const buffer = new Uint8Array( data.byteLength + value.byteLength, ); @@ -254,20 +98,6 @@ class CastDownloadManager { }); return stream; } - - trackDownloadProgress = (fileID: number) => { - return (event: { loaded: number; total: number }) => { - if (event.loaded === event.total) { - this.fileDownloadProgress.delete(fileID); - } else { - this.fileDownloadProgress.set( - fileID, - Math.round((event.loaded * 100) / event.total), - ); - } - this.progressUpdater(new Map(this.fileDownloadProgress)); - }; - }; } export default new CastDownloadManager(); diff --git a/web/apps/cast/src/services/events.ts b/web/apps/cast/src/services/events.ts deleted file mode 100644 index 32306fc64..000000000 --- a/web/apps/cast/src/services/events.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EventEmitter } from "eventemitter3"; - -// When registering event handlers, -// handle errors to avoid unhandled rejection or propagation to emit call - -export enum Events { - LOGOUT = "logout", - FILE_UPLOADED = "fileUploaded", - LOCAL_FILES_UPDATED = "localFilesUpdated", -} - -export const eventBus = new EventEmitter<Events>(); diff --git a/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts b/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts index b3c716d99..0f7d226c8 100644 --- a/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts +++ b/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts @@ -1,26 +1,19 @@ -// import isElectron from 'is-electron'; -// import { ElectronFFmpeg } from 'services/electron/ffmpeg'; -import { ElectronFile } from "types/upload"; import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker"; export interface IFFmpeg { run: ( cmd: string[], - inputFile: File | ElectronFile, + inputFile: File, outputFilename: string, dontTimeout?: boolean, - ) => Promise<File | ElectronFile>; + ) => Promise<File>; } class FFmpegFactory { private client: IFFmpeg; async getFFmpegClient() { if (!this.client) { - // if (isElectron()) { - // this.client = new ElectronFFmpeg(); - // } else { this.client = await ComlinkFFmpegWorker.getInstance(); - // } } return this.client; } diff --git a/web/apps/cast/src/services/ffmpeg/ffmpegService.ts b/web/apps/cast/src/services/ffmpeg/ffmpegService.ts index 85bab9939..325f1d66b 100644 --- a/web/apps/cast/src/services/ffmpeg/ffmpegService.ts +++ b/web/apps/cast/src/services/ffmpeg/ffmpegService.ts @@ -4,10 +4,9 @@ import { INPUT_PATH_PLACEHOLDER, OUTPUT_PATH_PLACEHOLDER, } from "constants/ffmpeg"; -import { ElectronFile } from "types/upload"; import ffmpegFactory from "./ffmpegFactory"; -export async function convertToMP4(file: File | ElectronFile) { +export async function convertToMP4(file: File) { try { const ffmpegClient = await ffmpegFactory.getFFmpegClient(); return await ffmpegClient.run( diff --git a/web/apps/cast/src/services/heicConversionService.ts b/web/apps/cast/src/services/heicConversionService.ts deleted file mode 100644 index f11a9f4a4..000000000 --- a/web/apps/cast/src/services/heicConversionService.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService"; - -class HeicConversionService { - async convert(heicFileData: Blob): Promise<Blob> { - try { - return await WasmHEICConverterService.convert(heicFileData); - } catch (e) { - logError(e, "failed to convert heic file"); - throw e; - } - } -} -export default new HeicConversionService(); diff --git a/web/apps/cast/src/services/livePhotoService.ts b/web/apps/cast/src/services/livePhotoService.ts index 4d96e812c..789234bd3 100644 --- a/web/apps/cast/src/services/livePhotoService.ts +++ b/web/apps/cast/src/services/livePhotoService.ts @@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => { } return livePhoto; }; - -export const encodeLivePhoto = async (livePhoto: LivePhoto) => { - const zip = new JSZip(); - zip.file( - "image" + getFileExtensionWithDot(livePhoto.imageNameTitle), - livePhoto.image, - ); - zip.file( - "video" + getFileExtensionWithDot(livePhoto.videoNameTitle), - livePhoto.video, - ); - return await zip.generateAsync({ type: "uint8array" }); -}; diff --git a/web/apps/cast/src/services/readerService.ts b/web/apps/cast/src/services/readerService.ts index 344fd9f20..7682f1580 100644 --- a/web/apps/cast/src/services/readerService.ts +++ b/web/apps/cast/src/services/readerService.ts @@ -1,10 +1,7 @@ import { logError } from "@ente/shared/sentry"; import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import { ElectronFile } from "types/upload"; -export async function getUint8ArrayView( - file: Blob | ElectronFile, -): Promise<Uint8Array> { +export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> { try { return new Uint8Array(await file.arrayBuffer()); } catch (e) { @@ -14,80 +11,3 @@ export async function getUint8ArrayView( throw e; } } - -export function getFileStream(file: File, chunkSize: number) { - const fileChunkReader = fileChunkReaderMaker(file, chunkSize); - - const stream = new ReadableStream<Uint8Array>({ - async pull(controller: ReadableStreamDefaultController) { - const chunk = await fileChunkReader.next(); - if (chunk.done) { - controller.close(); - } else { - controller.enqueue(chunk.value); - } - }, - }); - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream, - chunkCount, - }; -} - -export async function getElectronFileStream( - file: ElectronFile, - chunkSize: number, -) { - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream: await file.stream(), - chunkCount, - }; -} - -async function* fileChunkReaderMaker(file: File, chunkSize: number) { - let offset = 0; - while (offset < file.size) { - const blob = file.slice(offset, chunkSize + offset); - const fileChunk = await getUint8ArrayView(blob); - yield fileChunk; - offset += chunkSize; - } - return null; -} - -// depreciated -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function getUint8ArrayViewOld( - reader: FileReader, - file: Blob, -): Promise<Uint8Array> { - return await new Promise((resolve, reject) => { - reader.onabort = () => - reject( - Error( - `file reading was aborted, file size= ${convertBytesToHumanReadable( - file.size, - )}`, - ), - ); - reader.onerror = () => - reject( - Error( - `file reading has failed, file size= ${convertBytesToHumanReadable( - file.size, - )} , reason= ${reader.error}`, - ), - ); - reader.onload = () => { - // Do whatever you want with the file contents - const result = - typeof reader.result === "string" - ? new TextEncoder().encode(reader.result) - : new Uint8Array(reader.result); - resolve(result); - }; - reader.readAsArrayBuffer(file); - }); -} diff --git a/web/apps/cast/src/services/typeDetectionService.ts b/web/apps/cast/src/services/typeDetectionService.ts index c280baf51..826253ce4 100644 --- a/web/apps/cast/src/services/typeDetectionService.ts +++ b/web/apps/cast/src/services/typeDetectionService.ts @@ -6,37 +6,25 @@ import { KNOWN_NON_MEDIA_FORMATS, WHITELISTED_FILE_FORMATS, } from "constants/upload"; -import FileType, { FileTypeResult } from "file-type"; -import { ElectronFile, FileTypeInfo } from "types/upload"; +import FileType from "file-type"; +import { FileTypeInfo } from "types/upload"; import { getFileExtension } from "utils/file"; import { getUint8ArrayView } from "./readerService"; -function getFileSize(file: File | ElectronFile) { - return file.size; -} - const TYPE_VIDEO = "video"; const TYPE_IMAGE = "image"; const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100; -export async function getFileType( - receivedFile: File | ElectronFile, -): Promise<FileTypeInfo> { +export async function getFileType(receivedFile: File): Promise<FileTypeInfo> { try { let fileType: FILE_TYPE; - let typeResult: FileTypeResult; - - if (receivedFile instanceof File) { - typeResult = await extractFileType(receivedFile); - } else { - typeResult = await extractElectronFileType(receivedFile); - } + const typeResult = await extractFileType(receivedFile); const mimTypeParts: string[] = typeResult.mime?.split("/"); - if (mimTypeParts?.length !== 2) { throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime)); } + switch (mimTypeParts[0]) { case TYPE_IMAGE: fileType = FILE_TYPE.IMAGE; @@ -54,7 +42,7 @@ export async function getFileType( }; } catch (e) { const fileFormat = getFileExtension(receivedFile.name); - const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile)); + const fileSize = convertBytesToHumanReadable(receivedFile.size); const whiteListedFormat = WHITELISTED_FILE_FORMATS.find( (a) => a.exactType === fileFormat, ); @@ -85,14 +73,6 @@ async function extractFileType(file: File) { return getFileTypeFromBuffer(fileDataChunk); } -async function extractElectronFileType(file: ElectronFile) { - const stream = await file.stream(); - const reader = stream.getReader(); - const { value: fileDataChunk } = await reader.read(); - await reader.cancel(); - return getFileTypeFromBuffer(fileDataChunk); -} - async function getFileTypeFromBuffer(buffer: Uint8Array) { const result = await FileType.fromBuffer(buffer); if (!result?.mime) { diff --git a/web/apps/cast/src/services/wasm/ffmpeg.ts b/web/apps/cast/src/services/wasm/ffmpeg.ts index ce6f871ed..50ab5a5a9 100644 --- a/web/apps/cast/src/services/wasm/ffmpeg.ts +++ b/web/apps/cast/src/services/wasm/ffmpeg.ts @@ -1,10 +1,10 @@ import { addLogLine } from "@ente/shared/logging"; import { promiseWithTimeout } from "@ente/shared/promise"; import { logError } from "@ente/shared/sentry"; +import QueueProcessor from "@ente/shared/utils/queueProcessor"; +import { generateTempName } from "@ente/shared/utils/temp"; import { createFFmpeg, FFmpeg } from "ffmpeg-wasm"; -import QueueProcessor from "services/queueProcessor"; import { getUint8ArrayView } from "services/readerService"; -import { generateTempName } from "utils/temp"; const INPUT_PATH_PLACEHOLDER = "INPUT"; const FFMPEG_PLACEHOLDER = "FFMPEG"; diff --git a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts b/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts deleted file mode 100644 index 03b390fb9..000000000 --- a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as HeicConvert from "heic-convert"; -import { getUint8ArrayView } from "services/readerService"; - -export async function convertHEIC( - fileBlob: Blob, - format: string, -): Promise<Blob> { - const filedata = await getUint8ArrayView(fileBlob); - const result = await HeicConvert({ buffer: filedata, format }); - const convertedFileData = new Uint8Array(result); - const convertedFileBlob = new Blob([convertedFileData]); - return convertedFileBlob; -} diff --git a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts b/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts deleted file mode 100644 index a49d8e4f8..000000000 --- a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { CustomError } from "@ente/shared/error"; -import { addLogLine } from "@ente/shared/logging"; -import { logError } from "@ente/shared/sentry"; -import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import QueueProcessor from "services/queueProcessor"; -import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker"; -import { ComlinkWorker } from "utils/comlink/comlinkWorker"; -import { retryAsyncFunction } from "utils/network"; -import { DedicatedConvertWorker } from "worker/convert.worker"; - -const WORKER_POOL_SIZE = 2; -const MAX_CONVERSION_IN_PARALLEL = 1; -const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100]; -const WAIT_TIME_IN_MICROSECONDS = 30 * 1000; -const BREATH_TIME_IN_MICROSECONDS = 1000; -const CONVERT_FORMAT = "JPEG"; - -class HEICConverter { - private convertProcessor = new QueueProcessor<Blob>( - MAX_CONVERSION_IN_PARALLEL, - ); - private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = []; - private ready: Promise<void>; - - constructor() { - this.ready = this.init(); - } - private async init() { - this.workerPool = []; - for (let i = 0; i < WORKER_POOL_SIZE; i++) { - this.workerPool.push(getDedicatedConvertWorker()); - } - } - async convert(fileBlob: Blob): Promise<Blob> { - await this.ready; - const response = this.convertProcessor.queueUpRequest(() => - retryAsyncFunction<Blob>(async () => { - const convertWorker = this.workerPool.shift(); - const worker = await convertWorker.remote; - try { - const convertedHEIC = await new Promise<Blob>( - (resolve, reject) => { - const main = async () => { - try { - const timeout = setTimeout(() => { - reject(Error("wait time exceeded")); - }, WAIT_TIME_IN_MICROSECONDS); - const startTime = Date.now(); - const convertedHEIC = - await worker.convertHEIC( - fileBlob, - CONVERT_FORMAT, - ); - addLogLine( - `originalFileSize:${convertBytesToHumanReadable( - fileBlob?.size, - )},convertedFileSize:${convertBytesToHumanReadable( - convertedHEIC?.size, - )}, heic conversion time: ${ - Date.now() - startTime - }ms `, - ); - clearTimeout(timeout); - resolve(convertedHEIC); - } catch (e) { - reject(e); - } - }; - main(); - }, - ); - if (!convertedHEIC || convertedHEIC?.size === 0) { - logError( - Error(`converted heic fileSize is Zero`), - "converted heic fileSize is Zero", - { - originalFileSize: convertBytesToHumanReadable( - fileBlob?.size ?? 0, - ), - convertedFileSize: convertBytesToHumanReadable( - convertedHEIC?.size ?? 0, - ), - }, - ); - } - await new Promise((resolve) => { - setTimeout( - () => resolve(null), - BREATH_TIME_IN_MICROSECONDS, - ); - }); - this.workerPool.push(convertWorker); - return convertedHEIC; - } catch (e) { - logError(e, "heic conversion failed"); - convertWorker.terminate(); - this.workerPool.push(getDedicatedConvertWorker()); - throw e; - } - }, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS), - ); - try { - return await response.promise; - } catch (e) { - if (e.message === CustomError.REQUEST_CANCELLED) { - // ignore - return null; - } - throw e; - } - } -} - -export default new HEICConverter(); diff --git a/web/apps/cast/src/types/cache/index.ts b/web/apps/cast/src/types/cache/index.ts deleted file mode 100644 index 2920ece10..000000000 --- a/web/apps/cast/src/types/cache/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface LimitedCacheStorage { - open: (cacheName: string) => Promise<LimitedCache>; - delete: (cacheName: string) => Promise<boolean>; -} - -export interface LimitedCache { - match: (key: string) => Promise<Response>; - put: (key: string, data: Response) => Promise<void>; - delete: (key: string) => Promise<boolean>; -} - -export interface ProxiedLimitedCacheStorage { - open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>; - delete: (cacheName: string) => Promise<boolean>; -} -export interface ProxiedWorkerLimitedCache { - match: (key: string) => Promise<ArrayBuffer>; - put: (key: string, data: ArrayBuffer) => Promise<void>; - delete: (key: string) => Promise<boolean>; -} diff --git a/web/apps/cast/src/types/cast/index.ts b/web/apps/cast/src/types/cast/index.ts deleted file mode 100644 index f082e433e..000000000 --- a/web/apps/cast/src/types/cast/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CastPayload { - collectionID: number; - collectionKey: string; - castToken: string; -} diff --git a/web/apps/cast/src/types/gallery/index.ts b/web/apps/cast/src/types/gallery/index.ts deleted file mode 100644 index 0216825c8..000000000 --- a/web/apps/cast/src/types/gallery/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -// import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress'; -// import { CollectionSelectorAttributes } from 'components/Collections/CollectionSelector'; -// import { TimeStampListItem } from 'components/PhotoList'; -import { User } from "@ente/shared/user/types"; -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; - -export type SelectedState = { - [k: number]: boolean; - ownCount: number; - count: number; - collectionID: number; -}; -export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>; -export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>; -export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>; -// export type SetCollectionSelectorAttributes = React.Dispatch< -// React.SetStateAction<CollectionSelectorAttributes> -// >; -// export type SetCollectionDownloadProgressAttributes = React.Dispatch< -// React.SetStateAction<CollectionDownloadProgressAttributes> -// >; - -export type MergedSourceURL = { - original: string; - converted: string; -}; -export enum UploadTypeSelectorIntent { - normalUpload, - import, - collectPhotos, -} -export type GalleryContextType = { - thumbs: Map<number, string>; - files: Map<number, MergedSourceURL>; - showPlanSelectorModal: () => void; - setActiveCollectionID: (collectionID: number) => void; - syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>; - setBlockingLoad: (value: boolean) => void; - setIsInSearchMode: (value: boolean) => void; - // photoListHeader: TimeStampListItem; - openExportModal: () => void; - authenticateUser: (callback: () => void) => void; - user: User; - userIDToEmailMap: Map<number, string>; - emailList: string[]; - openHiddenSection: (callback?: () => void) => void; - isClipSearchResult: boolean; -}; - -export enum CollectionSelectorIntent { - upload, - add, - move, - restore, - unhide, -} diff --git a/web/apps/cast/src/types/upload/index.ts b/web/apps/cast/src/types/upload/index.ts index 0d38f6190..ef44b4a23 100644 --- a/web/apps/cast/src/types/upload/index.ts +++ b/web/apps/cast/src/types/upload/index.ts @@ -3,7 +3,6 @@ import { LocalFileAttributes, } from "@ente/shared/crypto/types"; import { FILE_TYPE } from "constants/file"; -import { Collection } from "types/collection"; import { FilePublicMagicMetadata, FilePublicMagicMetadataProps, @@ -39,24 +38,6 @@ export interface Metadata { deviceFolder?: string; } -export interface Location { - latitude: number; - longitude: number; -} - -export interface ParsedMetadataJSON { - creationTime: number; - modificationTime: number; - latitude: number; - longitude: number; -} - -export interface MultipartUploadURLs { - objectKey: string; - partURLs: string[]; - completeURL: string; -} - export interface FileTypeInfo { fileType: FILE_TYPE; exactType: string; @@ -65,43 +46,6 @@ export interface FileTypeInfo { videoType?: string; } -/* - * ElectronFile is a custom interface that is used to represent - * any file on disk as a File-like object in the Electron desktop app. - * - * This was added to support the auto-resuming of failed uploads - * which needed absolute paths to the files which the - * normal File interface does not provide. - */ -export interface ElectronFile { - name: string; - path: string; - size: number; - lastModified: number; - stream: () => Promise<ReadableStream<Uint8Array>>; - blob: () => Promise<Blob>; - arrayBuffer: () => Promise<Uint8Array>; -} - -export interface UploadAsset { - isLivePhoto?: boolean; - file?: File | ElectronFile; - livePhotoAssets?: LivePhotoAssets; - isElectron?: boolean; -} -export interface LivePhotoAssets { - image: globalThis.File | ElectronFile; - video: globalThis.File | ElectronFile; -} - -export interface FileWithCollection extends UploadAsset { - localID: number; - collection?: Collection; - collectionID?: number; -} - -export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>; - export interface UploadURL { url: string; objectKey: string; diff --git a/web/apps/cast/src/types/upload/ui.ts b/web/apps/cast/src/types/upload/ui.ts deleted file mode 100644 index bce381213..000000000 --- a/web/apps/cast/src/types/upload/ui.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { UPLOAD_RESULT, UPLOAD_STAGES } from "constants/upload"; - -export type FileID = number; -export type FileName = string; - -export type PercentageUploaded = number; -export type UploadFileNames = Map<FileID, FileName>; - -export interface UploadCounter { - finished: number; - total: number; -} - -export interface InProgressUpload { - localFileID: FileID; - progress: PercentageUploaded; -} - -export interface FinishedUpload { - localFileID: FileID; - result: UPLOAD_RESULT; -} - -export type InProgressUploads = Map<FileID, PercentageUploaded>; - -export type FinishedUploads = Map<FileID, UPLOAD_RESULT>; - -export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>; - -export interface ProgressUpdater { - setPercentComplete: React.Dispatch<React.SetStateAction<number>>; - setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>; - setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>; - setInProgressUploads: React.Dispatch< - React.SetStateAction<InProgressUpload[]> - >; - setFinishedUploads: React.Dispatch< - React.SetStateAction<SegregatedFinishedUploads> - >; - setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>; - setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>; - setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>; -} diff --git a/web/apps/cast/src/utils/collection/index.ts b/web/apps/cast/src/utils/collection/index.ts deleted file mode 100644 index bd6c2791d..000000000 --- a/web/apps/cast/src/utils/collection/index.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { - CollectionSummaryType, - CollectionType, - HIDE_FROM_COLLECTION_BAR_TYPES, - OPTIONS_NOT_HAVING_COLLECTION_TYPES, -} from "constants/collection"; -import { COLLECTION_ROLE, Collection } from "types/collection"; -import { SUB_TYPE, VISIBILITY_STATE } from "types/magicMetadata"; - -export enum COLLECTION_OPS_TYPE { - ADD, - MOVE, - REMOVE, - RESTORE, - UNHIDE, -} - -export function getSelectedCollection( - collectionID: number, - collections: Collection[], -) { - return collections.find((collection) => collection.id === collectionID); -} - -export const shouldShowOptions = (type: CollectionSummaryType) => { - return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type); -}; -export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => { - return type === CollectionSummaryType.trash; -}; -export const showDownloadQuickOption = (type: CollectionSummaryType) => { - return ( - type === CollectionSummaryType.folder || - type === CollectionSummaryType.favorites || - type === CollectionSummaryType.album || - type === CollectionSummaryType.uncategorized || - type === CollectionSummaryType.hiddenItems || - type === CollectionSummaryType.incomingShareViewer || - type === CollectionSummaryType.incomingShareCollaborator || - type === CollectionSummaryType.outgoingShare || - type === CollectionSummaryType.sharedOnlyViaLink || - type === CollectionSummaryType.archived || - type === CollectionSummaryType.pinned - ); -}; -export const showShareQuickOption = (type: CollectionSummaryType) => { - return ( - type === CollectionSummaryType.folder || - type === CollectionSummaryType.album || - type === CollectionSummaryType.outgoingShare || - type === CollectionSummaryType.sharedOnlyViaLink || - type === CollectionSummaryType.archived || - type === CollectionSummaryType.incomingShareViewer || - type === CollectionSummaryType.incomingShareCollaborator || - type === CollectionSummaryType.pinned - ); -}; -export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => { - return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type); -}; - -export const getUserOwnedCollections = (collections: Collection[]) => { - const user: User = getData(LS_KEYS.USER); - if (!user?.id) { - throw Error("user missing"); - } - return collections.filter((collection) => collection.owner.id === user.id); -}; - -export const isDefaultHiddenCollection = (collection: Collection) => - collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN; - -export const isHiddenCollection = (collection: Collection) => - collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN; - -export const isQuickLinkCollection = (collection: Collection) => - collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION; - -export function isOutgoingShare(collection: Collection, user: User): boolean { - return collection.owner.id === user.id && collection.sharees?.length > 0; -} - -export function isIncomingShare(collection: Collection, user: User) { - return collection.owner.id !== user.id; -} - -export function isIncomingViewerShare(collection: Collection, user: User) { - const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); - return sharee?.role === COLLECTION_ROLE.VIEWER; -} - -export function isIncomingCollabShare(collection: Collection, user: User) { - const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); - return sharee?.role === COLLECTION_ROLE.COLLABORATOR; -} - -export function isSharedOnlyViaLink(collection: Collection) { - return collection.publicURLs?.length && !collection.sharees?.length; -} - -export function isValidMoveTarget( - sourceCollectionID: number, - targetCollection: Collection, - user: User, -) { - return ( - sourceCollectionID !== targetCollection.id && - !isHiddenCollection(targetCollection) && - !isQuickLinkCollection(targetCollection) && - !isIncomingShare(targetCollection, user) - ); -} - -export function isValidReplacementAlbum( - collection: Collection, - user: User, - wantedCollectionName: string, -) { - return ( - collection.name === wantedCollectionName && - (collection.type === CollectionType.album || - collection.type === CollectionType.folder) && - !isHiddenCollection(collection) && - !isQuickLinkCollection(collection) && - !isIncomingShare(collection, user) - ); -} - -export function getCollectionNameMap( - collections: Collection[], -): Map<number, string> { - return new Map<number, string>( - collections.map((collection) => [collection.id, collection.name]), - ); -} - -export function getNonHiddenCollections( - collections: Collection[], -): Collection[] { - return collections.filter((collection) => !isHiddenCollection(collection)); -} - -export function getHiddenCollections(collections: Collection[]): Collection[] { - return collections.filter((collection) => isHiddenCollection(collection)); -} diff --git a/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts b/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts deleted file mode 100644 index dc15136d9..000000000 --- a/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { runningInBrowser } from "@ente/shared/platform"; -import { Remote } from "comlink"; -import { DedicatedConvertWorker } from "worker/convert.worker"; -import { ComlinkWorker } from "./comlinkWorker"; - -class ComlinkConvertWorker { - private comlinkWorkerInstance: Remote<DedicatedConvertWorker>; - - async getInstance() { - if (!this.comlinkWorkerInstance) { - this.comlinkWorkerInstance = - await getDedicatedConvertWorker().remote; - } - return this.comlinkWorkerInstance; - } -} - -export const getDedicatedConvertWorker = () => { - if (runningInBrowser()) { - const cryptoComlinkWorker = new ComlinkWorker< - typeof DedicatedConvertWorker - >( - "ente-convert-worker", - new Worker(new URL("worker/convert.worker.ts", import.meta.url)), - ); - return cryptoComlinkWorker; - } -}; - -export default new ComlinkConvertWorker(); diff --git a/web/apps/cast/src/utils/comlink/comlinkWorker.ts b/web/apps/cast/src/utils/comlink/comlinkWorker.ts index 9c1aacff6..e924a3a96 100644 --- a/web/apps/cast/src/utils/comlink/comlinkWorker.ts +++ b/web/apps/cast/src/utils/comlink/comlinkWorker.ts @@ -1,6 +1,5 @@ import { addLocalLog } from "@ente/shared/logging"; import { Remote, wrap } from "comlink"; -// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client'; export class ComlinkWorker<T extends new () => InstanceType<T>> { public remote: Promise<Remote<InstanceType<T>>>; @@ -17,7 +16,6 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> { addLocalLog(() => `Initiated ${this.name}`); const comlink = wrap<T>(this.worker); this.remote = new comlink() as Promise<Remote<InstanceType<T>>>; - // expose(WorkerElectronCacheStorageClient, this.worker); } public terminate() { diff --git a/web/apps/cast/src/utils/file/blob.ts b/web/apps/cast/src/utils/file/blob.ts deleted file mode 100644 index cb2e8c7a2..000000000 --- a/web/apps/cast/src/utils/file/blob.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const readAsDataURL = (blob) => - new Promise<string>((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = () => resolve(fileReader.result as string); - fileReader.onerror = () => reject(fileReader.error); - fileReader.readAsDataURL(blob); - }); - -export const readAsText = (blob) => - new Promise<string>((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = () => resolve(fileReader.result as string); - fileReader.onerror = () => reject(fileReader.error); - fileReader.readAsText(blob); - }); diff --git a/web/apps/cast/src/utils/file/index.ts b/web/apps/cast/src/utils/file/index.ts index 63672c0ef..9b9c8e21e 100644 --- a/web/apps/cast/src/utils/file/index.ts +++ b/web/apps/cast/src/utils/file/index.ts @@ -1,14 +1,6 @@ import { logError } from "@ente/shared/sentry"; -import { - FILE_TYPE, - RAW_FORMATS, - SUPPORTED_RAW_FORMATS, - TYPE_HEIC, - TYPE_HEIF, -} from "constants/file"; +import { FILE_TYPE, RAW_FORMATS } from "constants/file"; import CastDownloadManager from "services/castDownloadManager"; -import * as ffmpegService from "services/ffmpeg/ffmpegService"; -import heicConversionService from "services/heicConversionService"; import { decodeLivePhoto } from "services/livePhotoService"; import { getFileType } from "services/typeDetectionService"; import { @@ -17,58 +9,7 @@ import { FileMagicMetadata, FilePublicMagicMetadata, } from "types/file"; -import { SelectedState } from "types/gallery"; -import { isArchivedFile } from "utils/magicMetadata"; - -import { CustomError } from "@ente/shared/error"; -import { addLocalLog, addLogLine } from "@ente/shared/logging"; -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import isElectron from "is-electron"; -import { FileTypeInfo } from "types/upload"; import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; -import { isPlaybackPossible } from "utils/photoFrame"; - -const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000; - -export enum FILE_OPS_TYPE { - DOWNLOAD, - FIX_TIME, - ARCHIVE, - UNARCHIVE, - HIDE, - TRASH, - DELETE_PERMANENTLY, -} - -export function groupFilesBasedOnCollectionID(files: EnteFile[]) { - const collectionWiseFiles = new Map<number, EnteFile[]>(); - for (const file of files) { - if (!collectionWiseFiles.has(file.collectionID)) { - collectionWiseFiles.set(file.collectionID, []); - } - collectionWiseFiles.get(file.collectionID).push(file); - } - return collectionWiseFiles; -} - -function getSelectedFileIds(selectedFiles: SelectedState) { - const filesIDs: number[] = []; - for (const [key, val] of Object.entries(selectedFiles)) { - if (typeof val === "boolean" && val) { - filesIDs.push(Number(key)); - } - } - return new Set(filesIDs); -} -export function getSelectedFiles( - selected: SelectedState, - files: EnteFile[], -): EnteFile[] { - const selectedFilesIDs = getSelectedFileIds(selected); - return files.filter((file) => selectedFilesIDs.has(file.id)); -} export function sortFiles(files: EnteFile[], sortAsc = false) { // sort based on the time of creation time of the file, @@ -85,20 +26,6 @@ export function sortFiles(files: EnteFile[], sortAsc = false) { }); } -export function sortTrashFiles(files: EnteFile[]) { - return files.sort((a, b) => { - if (a.deleteBy === b.deleteBy) { - if (a.metadata.creationTime === b.metadata.creationTime) { - return ( - b.metadata.modificationTime - a.metadata.modificationTime - ); - } - return b.metadata.creationTime - a.metadata.creationTime; - } - return a.deleteBy - b.deleteBy; - }); -} - export async function decryptFile( file: EncryptedEnteFile, collectionKey: string, @@ -193,176 +120,6 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) { }); } -export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) { - switch (file.metadata.fileType) { - case FILE_TYPE.IMAGE: { - const convertedBlob = await getRenderableImage( - file.metadata.title, - fileBlob, - ); - const { originalURL, convertedURL } = getFileObjectURLs( - fileBlob, - convertedBlob, - ); - return { - converted: [convertedURL], - original: [originalURL], - }; - } - case FILE_TYPE.LIVE_PHOTO: { - return await getRenderableLivePhotoURL(file, fileBlob); - } - case FILE_TYPE.VIDEO: { - const convertedBlob = await getPlayableVideo( - file.metadata.title, - fileBlob, - ); - const { originalURL, convertedURL } = getFileObjectURLs( - fileBlob, - convertedBlob, - ); - return { - converted: [convertedURL], - original: [originalURL], - }; - } - default: { - const previewURL = await createTypedObjectURL( - fileBlob, - file.metadata.title, - ); - return { - converted: [previewURL], - original: [previewURL], - }; - } - } -} - -async function getRenderableLivePhotoURL( - file: EnteFile, - fileBlob: Blob, -): Promise<{ original: string[]; converted: string[] }> { - const livePhoto = await decodeLivePhoto(file, fileBlob); - const imageBlob = new Blob([livePhoto.image]); - const videoBlob = new Blob([livePhoto.video]); - const convertedImageBlob = await getRenderableImage( - livePhoto.imageNameTitle, - imageBlob, - ); - const convertedVideoBlob = await getPlayableVideo( - livePhoto.videoNameTitle, - videoBlob, - true, - ); - const { originalURL: originalImageURL, convertedURL: convertedImageURL } = - getFileObjectURLs(imageBlob, convertedImageBlob); - - const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } = - getFileObjectURLs(videoBlob, convertedVideoBlob); - return { - converted: [convertedImageURL, convertedVideoURL], - original: [originalImageURL, originalVideoURL], - }; -} - -export async function getPlayableVideo( - videoNameTitle: string, - videoBlob: Blob, - forceConvert = false, -) { - try { - const isPlayable = await isPlaybackPossible( - URL.createObjectURL(videoBlob), - ); - if (isPlayable && !forceConvert) { - return videoBlob; - } else { - if (!forceConvert && !isElectron()) { - return null; - } - addLogLine( - "video format not supported, converting it name:", - videoNameTitle, - ); - const mp4ConvertedVideo = await ffmpegService.convertToMP4( - new File([videoBlob], videoNameTitle), - ); - addLogLine("video successfully converted", videoNameTitle); - return new Blob([await mp4ConvertedVideo.arrayBuffer()]); - } - } catch (e) { - addLogLine("video conversion failed", videoNameTitle); - logError(e, "video conversion failed"); - return null; - } -} - -export async function getRenderableImage(fileName: string, imageBlob: Blob) { - let fileTypeInfo: FileTypeInfo; - try { - const tempFile = new File([imageBlob], fileName); - fileTypeInfo = await getFileType(tempFile); - addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`); - const { exactType } = fileTypeInfo; - let convertedImageBlob: Blob; - if (isRawFile(exactType)) { - try { - if (!isSupportedRawFormat(exactType)) { - throw Error(CustomError.UNSUPPORTED_RAW_FORMAT); - } - - if (!isElectron()) { - throw Error(CustomError.NOT_AVAILABLE_ON_WEB); - } - addLogLine( - `RawConverter called for ${fileName}-${convertBytesToHumanReadable( - imageBlob.size, - )}`, - ); - // convertedImageBlob = await imageProcessor.convertToJPEG( - // imageBlob, - // fileName - // ); - addLogLine(`${fileName} successfully converted`); - } catch (e) { - try { - if (!isFileHEIC(exactType)) { - throw e; - } - addLogLine( - `HEICConverter called for ${fileName}-${convertBytesToHumanReadable( - imageBlob.size, - )}`, - ); - convertedImageBlob = - await heicConversionService.convert(imageBlob); - addLogLine(`${fileName} successfully converted`); - } catch (e) { - throw Error(CustomError.NON_PREVIEWABLE_FILE); - } - } - return convertedImageBlob; - } else { - return imageBlob; - } - } catch (e) { - logError(e, "get Renderable Image failed", { fileTypeInfo }); - return null; - } -} - -export function isFileHEIC(exactType: string) { - return ( - exactType.toLowerCase().endsWith(TYPE_HEIC) || - exactType.toLowerCase().endsWith(TYPE_HEIF) - ); -} - -export function isRawFile(exactType: string) { - return RAW_FORMATS.includes(exactType.toLowerCase()); -} - export function isRawFileFromFileName(fileName: string) { for (const rawFormat of RAW_FORMATS) { if (fileName.toLowerCase().endsWith(rawFormat)) { @@ -372,10 +129,6 @@ export function isRawFileFromFileName(fileName: string) { return false; } -export function isSupportedRawFormat(exactType: string) { - return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase()); -} - export function mergeMetadata(files: EnteFile[]): EnteFile[] { return files.map((file) => { if (file.pubMagicMetadata?.data.editedTime) { @@ -389,187 +142,24 @@ export function mergeMetadata(files: EnteFile[]): EnteFile[] { }); } -export async function getFileFromURL(fileURL: string) { - const fileBlob = await (await fetch(fileURL)).blob(); - const fileFile = new File([fileBlob], "temp"); - return fileFile; -} - -export function getUniqueFiles(files: EnteFile[]) { - const idSet = new Set<number>(); - const uniqueFiles = files.filter((file) => { - if (!idSet.has(file.id)) { - idSet.add(file.id); - return true; - } else { - return false; - } - }); - - return uniqueFiles; -} - -export const isImageOrVideo = (fileType: FILE_TYPE) => - [FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType); - -export const getArchivedFiles = (files: EnteFile[]) => { - return files.filter(isArchivedFile).map((file) => file.id); -}; - -export const createTypedObjectURL = async (blob: Blob, fileName: string) => { - const type = await getFileType(new File([blob], fileName)); - return URL.createObjectURL(new Blob([blob], { type: type.mimeType })); -}; - -export const getUserOwnedFiles = (files: EnteFile[]) => { - const user: User = getData(LS_KEYS.USER); - if (!user?.id) { - throw Error("user missing"); - } - return files.filter((file) => file.ownerID === user.id); -}; - -// doesn't work on firefox -export const copyFileToClipboard = async (fileUrl: string) => { - const canvas = document.createElement("canvas"); - const canvasCTX = canvas.getContext("2d"); - const image = new Image(); - - const blobPromise = new Promise<Blob>((resolve, reject) => { - let timeout: NodeJS.Timeout = null; - try { - image.setAttribute("src", fileUrl); - image.onload = () => { - canvas.width = image.width; - canvas.height = image.height; - canvasCTX.drawImage(image, 0, 0, image.width, image.height); - canvas.toBlob( - (blob) => { - resolve(blob); - }, - "image/png", - 1, - ); - - clearTimeout(timeout); - }; - } catch (e) { - void logError(e, "failed to copy to clipboard"); - reject(e); - } finally { - clearTimeout(timeout); - } - timeout = setTimeout( - () => reject(Error(CustomError.WAIT_TIME_EXCEEDED)), - WAIT_TIME_IMAGE_CONVERSION, - ); - }); - - const { ClipboardItem } = window; - - await navigator.clipboard - .write([new ClipboardItem({ "image/png": blobPromise })]) - .catch((e) => logError(e, "failed to copy to clipboard")); -}; - -export function getLatestVersionFiles(files: EnteFile[]) { - const latestVersionFiles = new Map<string, EnteFile>(); - files.forEach((file) => { - const uid = `${file.collectionID}-${file.id}`; - if ( - !latestVersionFiles.has(uid) || - latestVersionFiles.get(uid).updationTime < file.updationTime - ) { - latestVersionFiles.set(uid, file); - } - }); - return Array.from(latestVersionFiles.values()).filter( - (file) => !file.isDeleted, - ); -} - -export function getPersonalFiles(files: EnteFile[], user: User) { - if (!user?.id) { - throw Error("user missing"); - } - return files.filter((file) => file.ownerID === user.id); -} - -export function getIDBasedSortedFiles(files: EnteFile[]) { - return files.sort((a, b) => a.id - b.id); -} - -export function constructFileToCollectionMap(files: EnteFile[]) { - const fileToCollectionsMap = new Map<number, number[]>(); - (files ?? []).forEach((file) => { - if (!fileToCollectionsMap.get(file.id)) { - fileToCollectionsMap.set(file.id, []); - } - fileToCollectionsMap.get(file.id).push(file.collectionID); - }); - return fileToCollectionsMap; -} - -export const shouldShowAvatar = (file: EnteFile, user: User) => { - if (!file || !user) { - return false; - } - // is Shared file - else if (file.ownerID !== user.id) { - return true; - } - // is public collected file - else if ( - file.ownerID === user.id && - file.pubMagicMetadata?.data?.uploaderName - ) { - return true; - } else { - return false; - } -}; - export const getPreviewableImage = async ( file: EnteFile, castToken: string, ): Promise<Blob> => { try { - let fileBlob: Blob; - const fileURL = - await CastDownloadManager.getCachedOriginalFile(file)[0]; - if (!fileURL) { - fileBlob = await new Response( - await CastDownloadManager.downloadFile(castToken, file), - ).blob(); - } else { - fileBlob = await (await fetch(fileURL)).blob(); - } + let fileBlob = await new Response( + await CastDownloadManager.downloadFile(castToken, file), + ).blob(); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { const livePhoto = await decodeLivePhoto(file, fileBlob); fileBlob = new Blob([livePhoto.image]); } - const convertedBlob = await getRenderableImage( - file.metadata.title, - fileBlob, - ); - fileBlob = convertedBlob; const fileType = await getFileType( new File([fileBlob], file.metadata.title), ); - fileBlob = new Blob([fileBlob], { type: fileType.mimeType }); return fileBlob; } catch (e) { logError(e, "failed to download file"); } }; - -const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => { - const originalURL = URL.createObjectURL(originalBlob); - const convertedURL = convertedBlob - ? convertedBlob === originalBlob - ? originalURL - : URL.createObjectURL(convertedBlob) - : null; - return { originalURL, convertedURL }; -}; diff --git a/web/apps/cast/src/utils/file/livePhoto.ts b/web/apps/cast/src/utils/file/livePhoto.ts deleted file mode 100644 index 7d687217c..000000000 --- a/web/apps/cast/src/utils/file/livePhoto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FILE_TYPE } from "constants/file"; -import { getFileExtension } from "utils/file"; - -const IMAGE_EXTENSIONS = [ - "heic", - "heif", - "jpeg", - "jpg", - "png", - "gif", - "bmp", - "tiff", - "webp", -]; - -const VIDEO_EXTENSIONS = [ - "mov", - "mp4", - "m4v", - "avi", - "wmv", - "flv", - "mkv", - "webm", - "3gp", - "3g2", - "avi", - "ogv", - "mpg", - "mp", -]; - -export function getFileTypeFromExtensionForLivePhotoClustering( - filename: string, -) { - const extension = getFileExtension(filename)?.toLowerCase(); - if (IMAGE_EXTENSIONS.includes(extension)) { - return FILE_TYPE.IMAGE; - } else if (VIDEO_EXTENSIONS.includes(extension)) { - return FILE_TYPE.VIDEO; - } -} diff --git a/web/apps/cast/src/utils/magicMetadata/index.ts b/web/apps/cast/src/utils/magicMetadata/index.ts deleted file mode 100644 index 7beb45772..000000000 --- a/web/apps/cast/src/utils/magicMetadata/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; -import { MagicMetadataCore, VISIBILITY_STATE } from "types/magicMetadata"; -import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; - -export function isArchivedFile(item: EnteFile): boolean { - if (!item || !item.magicMetadata || !item.magicMetadata.data) { - return false; - } - return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED; -} - -export function isArchivedCollection(item: Collection): boolean { - if (!item) { - return false; - } - - if (item.magicMetadata && item.magicMetadata.data) { - return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED; - } - - if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) { - return ( - item.sharedMagicMetadata.data.visibility === - VISIBILITY_STATE.ARCHIVED - ); - } - return false; -} - -export function isPinnedCollection(item: Collection) { - if ( - !item || - !item.magicMetadata || - !item.magicMetadata.data || - typeof item.magicMetadata.data === "string" || - typeof item.magicMetadata.data.order === "undefined" - ) { - return false; - } - return item.magicMetadata.data.order !== 0; -} - -export async function updateMagicMetadata<T>( - magicMetadataUpdates: T, - originalMagicMetadata?: MagicMetadataCore<T>, - decryptionKey?: string, -): Promise<MagicMetadataCore<T>> { - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - - if (!originalMagicMetadata) { - originalMagicMetadata = getNewMagicMetadata<T>(); - } - - if (typeof originalMagicMetadata?.data === "string") { - originalMagicMetadata.data = await cryptoWorker.decryptMetadata( - originalMagicMetadata.data, - originalMagicMetadata.header, - decryptionKey, - ); - } - // copies the existing magic metadata properties of the files and updates the visibility value - // The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want - const magicMetadataProps: T = { - ...originalMagicMetadata.data, - ...magicMetadataUpdates, - }; - - const nonEmptyMagicMetadataProps = - getNonEmptyMagicMetadataProps(magicMetadataProps); - - const magicMetadata = { - ...originalMagicMetadata, - data: nonEmptyMagicMetadataProps, - count: Object.keys(nonEmptyMagicMetadataProps).length, - }; - - return magicMetadata; -} - -export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => { - return { - version: 1, - data: null, - header: null, - count: 0, - }; -}; - -export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => { - return Object.fromEntries( - Object.entries(magicMetadataProps).filter( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, v]) => v !== null && v !== undefined, - ), - ) as T; -}; diff --git a/web/apps/cast/src/utils/network/index.ts b/web/apps/cast/src/utils/network/index.ts deleted file mode 100644 index f7bac98ec..000000000 --- a/web/apps/cast/src/utils/network/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sleep } from "@ente/shared/sleep"; - -const waitTimeBeforeNextAttemptInMilliSeconds = [2000, 5000, 10000]; - -export async function retryAsyncFunction<T>( - request: (abort?: () => void) => Promise<T>, - waitTimeBeforeNextTry?: number[], -): Promise<T> { - if (!waitTimeBeforeNextTry) { - waitTimeBeforeNextTry = waitTimeBeforeNextAttemptInMilliSeconds; - } - - for ( - let attemptNumber = 0; - attemptNumber <= waitTimeBeforeNextTry.length; - attemptNumber++ - ) { - try { - const resp = await request(); - return resp; - } catch (e) { - if (attemptNumber === waitTimeBeforeNextTry.length) { - throw e; - } - await sleep(waitTimeBeforeNextTry[attemptNumber]); - } - } -} diff --git a/web/apps/cast/src/utils/photoFrame/index.ts b/web/apps/cast/src/utils/photoFrame/index.ts deleted file mode 100644 index 0cb6fc201..000000000 --- a/web/apps/cast/src/utils/photoFrame/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import { FILE_TYPE } from "constants/file"; -import { EnteFile } from "types/file"; -import { MergedSourceURL } from "types/gallery"; - -const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000; - -export async function isPlaybackPossible(url: string): Promise<boolean> { - return await new Promise((resolve) => { - const t = setTimeout(() => { - resolve(false); - }, WAIT_FOR_VIDEO_PLAYBACK); - - const video = document.createElement("video"); - video.addEventListener("canplay", function () { - clearTimeout(t); - video.remove(); // Clean up the video element - // also check for duration > 0 to make sure it is not a broken video - if (video.duration > 0) { - resolve(true); - } else { - resolve(false); - } - }); - video.addEventListener("error", function () { - clearTimeout(t); - video.remove(); - resolve(false); - }); - - video.src = url; - }); -} - -export async function playVideo(livePhotoVideo, livePhotoImage) { - const videoPlaying = !livePhotoVideo.paused; - if (videoPlaying) return; - livePhotoVideo.style.opacity = 1; - livePhotoImage.style.opacity = 0; - livePhotoVideo.load(); - livePhotoVideo.play().catch(() => { - pauseVideo(livePhotoVideo, livePhotoImage); - }); -} - -export async function pauseVideo(livePhotoVideo, livePhotoImage) { - const videoPlaying = !livePhotoVideo.paused; - if (!videoPlaying) return; - livePhotoVideo.pause(); - livePhotoVideo.style.opacity = 0; - livePhotoImage.style.opacity = 1; -} - -export function updateFileMsrcProps(file: EnteFile, url: string) { - file.msrc = url; - file.isSourceLoaded = false; - file.conversionFailed = false; - file.isConverted = false; - if (file.metadata.fileType === FILE_TYPE.IMAGE) { - file.src = url; - } else { - file.html = ` - <div class = 'pswp-item-container'> - <img src="${url}"/> - </div> - `; - } -} - -export async function updateFileSrcProps( - file: EnteFile, - mergedURL: MergedSourceURL, -) { - const urls = { - original: mergedURL.original.split(","), - converted: mergedURL.converted.split(","), - }; - let originalImageURL; - let originalVideoURL; - let convertedImageURL; - let convertedVideoURL; - let originalURL; - let isConverted; - let conversionFailed; - if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - [originalImageURL, originalVideoURL] = urls.original; - [convertedImageURL, convertedVideoURL] = urls.converted; - isConverted = - originalVideoURL !== convertedVideoURL || - originalImageURL !== convertedImageURL; - conversionFailed = !convertedVideoURL || !convertedImageURL; - } else if (file.metadata.fileType === FILE_TYPE.VIDEO) { - [originalVideoURL] = urls.original; - [convertedVideoURL] = urls.converted; - isConverted = originalVideoURL !== convertedVideoURL; - conversionFailed = !convertedVideoURL; - } else if (file.metadata.fileType === FILE_TYPE.IMAGE) { - [originalImageURL] = urls.original; - [convertedImageURL] = urls.converted; - isConverted = originalImageURL !== convertedImageURL; - conversionFailed = !convertedImageURL; - } else { - [originalURL] = urls.original; - isConverted = false; - conversionFailed = false; - } - - const isPlayable = !isConverted || (isConverted && !conversionFailed); - - file.w = window.innerWidth; - file.h = window.innerHeight; - file.isSourceLoaded = true; - file.originalImageURL = originalImageURL; - file.originalVideoURL = originalVideoURL; - file.isConverted = isConverted; - file.conversionFailed = conversionFailed; - - if (!isPlayable) { - return; - } - - if (file.metadata.fileType === FILE_TYPE.VIDEO) { - file.html = ` - <video controls onContextMenu="return false;"> - <source src="${convertedVideoURL}" /> - Your browser does not support the video tag. - </video> - `; - } else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - file.html = ` - <div class = 'pswp-item-container'> - <img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/> - <video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;"> - <source src="${convertedVideoURL}" /> - Your browser does not support the video tag. - </video> - </div> - `; - } else if (file.metadata.fileType === FILE_TYPE.IMAGE) { - file.src = convertedImageURL; - } else { - logError( - Error(`unknown file type - ${file.metadata.fileType}`), - "Unknown file type", - ); - file.src = originalURL; - } -} diff --git a/web/apps/cast/src/utils/time/format.ts b/web/apps/cast/src/utils/time/format.ts deleted file mode 100644 index 0e2dc68b5..000000000 --- a/web/apps/cast/src/utils/time/format.ts +++ /dev/null @@ -1,78 +0,0 @@ -import i18n, { t } from "i18next"; - -const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, { - weekday: "short", - month: "short", - day: "numeric", -}); - -const dateTimeFullFormatter2 = new Intl.DateTimeFormat(i18n.language, { - year: "numeric", -}); -const dateTimeShortFormatter = new Intl.DateTimeFormat(i18n.language, { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", -}); - -const timeFormatter = new Intl.DateTimeFormat(i18n.language, { - timeStyle: "short", -}); - -export function formatDateFull(date: number | Date) { - return [dateTimeFullFormatter1, dateTimeFullFormatter2] - .map((f) => f.format(date)) - .join(" "); -} - -export function formatDate(date: number | Date) { - const withinYear = - new Date().getFullYear() === new Date(date).getFullYear(); - const dateTimeFormat2 = !withinYear ? dateTimeFullFormatter2 : null; - return [dateTimeFullFormatter1, dateTimeFormat2] - .filter((f) => !!f) - .map((f) => f.format(date)) - .join(" "); -} - -export function formatDateTimeShort(date: number | Date) { - return dateTimeShortFormatter.format(date); -} - -export function formatTime(date: number | Date) { - return timeFormatter.format(date).toUpperCase(); -} - -export function formatDateTimeFull(dateTime: number | Date): string { - return [formatDateFull(dateTime), t("at"), formatTime(dateTime)].join(" "); -} - -export function formatDateTime(dateTime: number | Date): string { - return [formatDate(dateTime), t("at"), formatTime(dateTime)].join(" "); -} - -export function formatDateRelative(date: number) { - const units = { - year: 24 * 60 * 60 * 1000 * 365, - month: (24 * 60 * 60 * 1000 * 365) / 12, - day: 24 * 60 * 60 * 1000, - hour: 60 * 60 * 1000, - minute: 60 * 1000, - second: 1000, - }; - const relativeDateFormat = new Intl.RelativeTimeFormat(i18n.language, { - localeMatcher: "best fit", - numeric: "always", - style: "long", - }); - const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios - - for (const u in units) - if (Math.abs(elapsed) > units[u] || u === "second") - return relativeDateFormat.format( - Math.round(elapsed / units[u]), - u as Intl.RelativeTimeFormatUnit, - ); -} diff --git a/web/apps/cast/src/utils/time/index.ts b/web/apps/cast/src/utils/time/index.ts deleted file mode 100644 index 592d2c7af..000000000 --- a/web/apps/cast/src/utils/time/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -export interface TimeDelta { - hours?: number; - days?: number; - months?: number; - years?: number; -} - -interface DateComponent<T = number> { - year: T; - month: T; - day: T; - hour: T; - minute: T; - second: T; -} - -export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) { - if (!dateTime || isNaN(dateTime.getTime())) { - return null; - } - const unixTime = dateTime.getTime() * 1000; - //ignoring dateTimeString = "0000:00:00 00:00:00" - if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) { - return null; - } else if (unixTime > Date.now() * 1000) { - return null; - } else { - return unixTime; - } -} - -/* -generates data component for date in format YYYYMMDD-HHMMSS - */ -export function parseDateFromFusedDateString(dateTime: string) { - const dateComponent: DateComponent<number> = convertDateComponentToNumber({ - year: dateTime.slice(0, 4), - month: dateTime.slice(4, 6), - day: dateTime.slice(6, 8), - hour: dateTime.slice(9, 11), - minute: dateTime.slice(11, 13), - second: dateTime.slice(13, 15), - }); - return validateAndGetDateFromComponents(dateComponent); -} - -/* sample date format = 2018-08-19 12:34:45 - the date has six symbol separated number values - which we would extract and use to form the date - */ -export function tryToParseDateTime(dateTime: string): Date { - const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime); - if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) { - // the filename has size 8 consecutive and then 6 consecutive digits - // high possibility that the it is a date in format YYYYMMDD-HHMMSS - const possibleDateTime = dateComponent.year + "-" + dateComponent.month; - return parseDateFromFusedDateString(possibleDateTime); - } - return validateAndGetDateFromComponents( - convertDateComponentToNumber(dateComponent), - ); -} - -function getDateComponentsFromSymbolJoinedString( - dateTime: string, -): DateComponent<string> { - const [year, month, day, hour, minute, second] = - dateTime.match(/\d+/g) ?? []; - - return { year, month, day, hour, minute, second }; -} - -function validateAndGetDateFromComponents( - dateComponent: DateComponent<number>, -) { - let date = getDateFromComponents(dateComponent); - if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) { - // if the date has time values but they are not valid - // then we remove the time values and try to validate the date - date = getDateFromComponents(removeTimeValues(dateComponent)); - } - if (!isDatePartValid(date, dateComponent)) { - return null; - } - return date; -} - -function isTimePartValid(date: Date, dateComponent: DateComponent<number>) { - return ( - date.getHours() === dateComponent.hour && - date.getMinutes() === dateComponent.minute && - date.getSeconds() === dateComponent.second - ); -} - -function isDatePartValid(date: Date, dateComponent: DateComponent<number>) { - return ( - date.getFullYear() === dateComponent.year && - date.getMonth() === dateComponent.month && - date.getDate() === dateComponent.day - ); -} - -function convertDateComponentToNumber( - dateComponent: DateComponent<string>, -): DateComponent<number> { - return { - year: Number(dateComponent.year), - // https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor - month: Number(dateComponent.month) - 1, - day: Number(dateComponent.day), - hour: Number(dateComponent.hour), - minute: Number(dateComponent.minute), - second: Number(dateComponent.second), - }; -} - -function getDateFromComponents(dateComponent: DateComponent<number>) { - const { year, month, day, hour, minute, second } = dateComponent; - if (hasTimeValues(dateComponent)) { - return new Date(year, month, day, hour, minute, second); - } else { - return new Date(year, month, day); - } -} - -function hasTimeValues(dateComponent: DateComponent<number>) { - const { hour, minute, second } = dateComponent; - return !isNaN(hour) && !isNaN(minute) && !isNaN(second); -} - -function removeTimeValues( - dateComponent: DateComponent<number>, -): DateComponent<number> { - return { ...dateComponent, hour: 0, minute: 0, second: 0 }; -} diff --git a/web/apps/cast/src/worker/convert.worker.ts b/web/apps/cast/src/worker/convert.worker.ts deleted file mode 100644 index a805752ac..000000000 --- a/web/apps/cast/src/worker/convert.worker.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Comlink from "comlink"; -import { convertHEIC } from "services/wasmHeicConverter/wasmHEICConverterClient"; - -export class DedicatedConvertWorker { - async convertHEIC(fileBlob: Blob, format: string) { - return convertHEIC(fileBlob, format); - } -} - -Comlink.expose(DedicatedConvertWorker, self); diff --git a/web/apps/payments/.eslintrc.js b/web/apps/payments/.eslintrc.js new file mode 100644 index 000000000..56670a6a7 --- /dev/null +++ b/web/apps/payments/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + extends: ["@/build-config/eslintrc-next"], + parserOptions: { + tsconfigRootDir: __dirname, + }, + // TODO (MR): Figure out a way to not have to ignored the next config .js + // ignorePatterns: [".eslintrc.js", "next.config.js"], +}; diff --git a/web/apps/payments/README.md b/web/apps/payments/README.md new file mode 100644 index 000000000..e3fb4fd85 --- /dev/null +++ b/web/apps/payments/README.md @@ -0,0 +1,90 @@ +Code that runs on `payments.ente.io`. It brokers between our services and +Stripe's API for payments. + +## Development + +There are three pieces that need to be connected to have a working local setup: + +- A client app +- This web app +- Museum + +### Client app + +For the client, let us consider the Photos web app (similar configuration can be +done in the mobile client too). + +Add the following to `web/apps/photos/.env.local`: + +```env +NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 +NEXT_PUBLIC_ENTE_PAYMENTS_ENDPOINT = http://localhost:3001 +``` +Then start it locally + +```sh +yarn dev:photos +``` + +This tells it to connect to the museum and payments app running on localhost. + +> For connecting from the mobile app, you'll need to run museum on a local IP +> instead localhost. If so, just replace "http://localhost:8080" with (say) +> "http://192.168.1.2:8080" wherever mentioned. + +### Payments app + +For this (payments) web app, configure it to connect to the local museum, and +use a set of (development) Stripe keys which can be found in [Stripe's developer +dashboard](https://dashboard.stripe.com). + +Add the following to `web/apps/payments/.env.local`: + +```env +NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 +NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY = stripe_publishable_key +``` + +Then start it locally + +```sh +yarn dev:payments +``` + +### Museum + +1. Install the [stripe-cli](https://docs.stripe.com/stripe-cli) and capture the + webhook signing secret. + +2. Define this secret within your `musuem.yaml` + +3. Update the `whitelisted-redirect-urls` so that it supports redirecting to + the locally running payments app. + +Assuming that your local payments app is running on `localhost:3001`, your +`server/museum.yaml` should look as follows. + +```yaml +stripe: + us: + key: stripe_dev_key + webhook-secret: stripe_dev_webhook_secret + whitelisted-redirect-urls: ["http://localhost:3000/gallery", "http://192.168.1.2:3001/frameRedirect"] + path: + success: ?status=success&session_id={CHECKOUT_SESSION_ID} + cancel: ?status=fail&reason=canceled +``` + +Make sure you have test plans available for museum to use, by placing them in +(say) `server/data/billing/us-testing.json`. + +Finally, start museum, for example: + +``` +docker compose up +``` + +Now if you try to purchase a plan from your locally running photos web client, +it should redirect to the locally running payments app, and from there to +Stripe. Once the test purchase completes it should redirect back to the local +web client. diff --git a/web/apps/payments/next.config.js b/web/apps/payments/next.config.js new file mode 100644 index 000000000..64a1e9867 --- /dev/null +++ b/web/apps/payments/next.config.js @@ -0,0 +1,18 @@ +// @ts-check + +/** + * Configuration for the Next.js build + * + * See also: + * - packages/next/next.config.base.js + * - https://nextjs.org/docs/pages/api-reference/next-config-js + * + * @type {import("next").NextConfig} + */ +const nextConfig = { + /* generate a static export when we run `next build` */ + output: "export", + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/web/apps/payments/package.json b/web/apps/payments/package.json new file mode 100644 index 000000000..e73f26c6c --- /dev/null +++ b/web/apps/payments/package.json @@ -0,0 +1,10 @@ +{ + "name": "payments", + "version": "0.0.0", + "private": true, + "dependencies": { + "@/next": "*", + "@stripe/stripe-js": "^1.17.0", + "axios": "^1.6.7" + } +} diff --git a/web/apps/payments/src/components/Container.tsx b/web/apps/payments/src/components/Container.tsx new file mode 100644 index 000000000..bf8c57b5d --- /dev/null +++ b/web/apps/payments/src/components/Container.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Container: React.FC<React.PropsWithChildren> = ({ children }) => ( + <div className="container">{children}</div> +); diff --git a/web/apps/payments/src/components/Spinner.tsx b/web/apps/payments/src/components/Spinner.tsx new file mode 100644 index 000000000..2b4872696 --- /dev/null +++ b/web/apps/payments/src/components/Spinner.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Spinner: React.FC = () => { + return <div className="loading-spinner"></div>; +}; diff --git a/web/apps/payments/src/pages/404.tsx b/web/apps/payments/src/pages/404.tsx new file mode 100644 index 000000000..2f6f5d9d0 --- /dev/null +++ b/web/apps/payments/src/pages/404.tsx @@ -0,0 +1,7 @@ +import { Container } from "components/Container"; +import React from "react"; +import constants from "utils/strings"; + +export default function Home() { + return <Container>{constants.NOT_FOUND}</Container>; +} diff --git a/web/apps/payments/src/pages/_app.tsx b/web/apps/payments/src/pages/_app.tsx new file mode 100644 index 000000000..9d57985e9 --- /dev/null +++ b/web/apps/payments/src/pages/_app.tsx @@ -0,0 +1,18 @@ +import type { AppProps } from "next/app"; +import Head from "next/head"; +import React from "react"; +import constants from "utils/strings"; +import "../styles/globals.css"; + +function MyApp({ Component, pageProps }: AppProps) { + return ( + <> + <Head> + <title>{constants.TITLE} + + + + ); +} + +export default MyApp; diff --git a/web/apps/payments/src/pages/desktop-redirect.tsx b/web/apps/payments/src/pages/desktop-redirect.tsx new file mode 100644 index 000000000..89df55bb6 --- /dev/null +++ b/web/apps/payments/src/pages/desktop-redirect.tsx @@ -0,0 +1,18 @@ +import { Container } from "components/Container"; +import { Spinner } from "components/Spinner"; +import * as React from "react"; + +export default function DesktopRedirect() { + React.useEffect(() => { + const currentURL = new URL(window.location.href); + const desktopRedirectURL = new URL("ente://app/gallery"); + desktopRedirectURL.search = currentURL.search; + window.location.href = desktopRedirectURL.href; + }, []); + + return ( + + + + ); +} diff --git a/web/apps/payments/src/pages/index.tsx b/web/apps/payments/src/pages/index.tsx new file mode 100644 index 000000000..64c8b96a7 --- /dev/null +++ b/web/apps/payments/src/pages/index.tsx @@ -0,0 +1,42 @@ +import { Container } from "components/Container"; +import { Spinner } from "components/Spinner"; +import * as React from "react"; +import { parseAndHandleRequest } from "services/billingService"; +import { CUSTOM_ERROR } from "utils/error"; +import constants from "utils/strings"; + +export default function Home() { + const [errorMessageView, setErrorMessageView] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + async function main() { + try { + setLoading(true); + await parseAndHandleRequest(); + } catch (e: unknown) { + if ( + e instanceof Error && + e.message === CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS + ) { + window.location.href = "https://ente.io"; + } else { + setErrorMessageView(true); + } + } + } + // TODO: audit + // eslint-disable-next-line @typescript-eslint/no-floating-promises + main(); + }, []); + + return ( + + {errorMessageView ? ( +
{constants.SOMETHING_WENT_WRONG}
+ ) : ( + loading && + )} +
+ ); +} diff --git a/web/apps/payments/src/services/HTTPService.ts b/web/apps/payments/src/services/HTTPService.ts new file mode 100644 index 000000000..834a18ae6 --- /dev/null +++ b/web/apps/payments/src/services/HTTPService.ts @@ -0,0 +1,185 @@ +// TODO: Audit +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/consistent-indexed-object-style */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import axios, { AxiosRequestConfig } from "axios"; + +interface IHTTPHeaders { + [headerKey: string]: any; +} + +interface IQueryPrams { + [paramName: string]: any; +} + +/** + * Service to manage all HTTP calls. + */ +class HTTPService { + constructor() { + axios.interceptors.response.use( + (response) => Promise.resolve(response), + (err) => { + if (!err.response) { + return Promise.reject(err); + } + const { response } = err; + return Promise.reject(response); + }, + ); + } + + /** + * header object to be append to all api calls. + */ + private headers: IHTTPHeaders = { + "content-type": "application/json", + }; + + /** + * Sets the headers to the given object. + */ + public setHeaders(headers: IHTTPHeaders) { + this.headers = headers; + } + + /** + * Adds a header to list of headers. + */ + public appendHeader(key: string, value: string) { + this.headers = { + ...this.headers, + [key]: value, + }; + } + + /** + * Removes the given header. + */ + public removeHeader(key: string) { + this.headers[key] = undefined; + } + + /** + * Returns axios interceptors. + */ + // eslint-disable-next-line class-methods-use-this + public getInterceptors() { + return axios.interceptors; + } + + /** + * Generic HTTP request. + * This is done so that developer can use any functionality + * provided by axios. Here, only the set headers are spread + * over what was sent in config. + */ + public async request(config: AxiosRequestConfig, customConfig?: any) { + // eslint-disable-next-line no-param-reassign + config.headers = { + ...this.headers, + ...config.headers, + }; + if (customConfig?.cancel) { + config.cancelToken = new axios.CancelToken( + (c) => (customConfig.cancel.exec = c), + ); + } + return await axios({ ...config, ...customConfig }); + } + + /** + * Get request. + */ + public get( + url: string, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + headers, + method: "GET", + params, + url, + }, + customConfig, + ); + } + + /** + * Post request + */ + public post( + url: string, + data?: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "POST", + params, + url, + }, + customConfig, + ); + } + + /** + * Put request + */ + public put( + url: string, + data: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "PUT", + params, + url, + }, + customConfig, + ); + } + + /** + * Delete request + */ + public delete( + url: string, + data: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "DELETE", + params, + url, + }, + customConfig, + ); + } +} + +// Creates a Singleton Service. +// This will help me maintain common headers / functionality +// at a central place. +export default new HTTPService(); diff --git a/web/apps/payments/src/services/billingService.ts b/web/apps/payments/src/services/billingService.ts new file mode 100644 index 000000000..9224129d3 --- /dev/null +++ b/web/apps/payments/src/services/billingService.ts @@ -0,0 +1,287 @@ +// TODO: Audit this and other eslints +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-confusing-void-expression */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +import { loadStripe } from "@stripe/stripe-js"; +import { CUSTOM_ERROR } from "utils/error"; +import { logError } from "utils/log"; +import HTTPService from "./HTTPService"; + +const getStripePublishableKey = (stripeAccount: StripeAccountCountry) => { + if (stripeAccount === StripeAccountCountry.STRIPE_IN) { + return ( + process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ?? + "pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC" + ); + } else if (stripeAccount === StripeAccountCountry.STRIPE_US) { + return ( + process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ?? + "pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi" + ); + } else { + throw Error("stripe account not found"); + } +}; + +const getEndpoint = () => { + const endPoint = + process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? "https://api.ente.io"; + return endPoint; +}; +enum PAYMENT_INTENT_STATUS { + SUCCESS = "success", + REQUIRE_ACTION = "requires_action", + REQUIRE_PAYMENT_METHOD = "requires_payment_method", +} + +enum FAILURE_REASON { + // Unable to authenticate card or 3DS + // User should be showing button for fixing card via customer portal + AUTHENTICATION_FAILED = "authentication_failed", + // Card declined result in this error. Show button to the customer portal. + REQUIRE_PAYMENT_METHOD = "requires_payment_method", + STRIPE_ERROR = "stripe_error", + CANCELED = "canceled", + SERVER_ERROR = "server_error", +} + +enum STRIPE_ERROR_TYPE { + CARD_ERROR = "card_error", + AUTHENTICATION_ERROR = "authentication_error", +} + +enum STRIPE_ERROR_CODE { + AUTHENTICATION_ERROR = "payment_intent_authentication_failure", +} + +enum RESPONSE_STATUS { + success = "success", + fail = "fail", +} + +enum PaymentActionType { + Buy = "buy", + Update = "update", +} + +enum StripeAccountCountry { + STRIPE_IN = "IN", + STRIPE_US = "US", +} + +interface SubscriptionUpdateResponse { + result: { + status: PAYMENT_INTENT_STATUS; + clientSecret: string; + }; +} + +export async function parseAndHandleRequest() { + try { + const urlParams = new URLSearchParams(window.location.search); + const productID = urlParams.get("productID"); + const paymentToken = urlParams.get("paymentToken"); + const action = urlParams.get("action"); + const redirectURL = urlParams.get("redirectURL"); + if (!action && !paymentToken && !productID && !redirectURL) { + throw Error(CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS); + } else if (!action || !paymentToken || !productID || !redirectURL) { + throw Error(CUSTOM_ERROR.MISSING_REQUIRED_QUERY_PARAM); + } + switch (action) { + case PaymentActionType.Buy: + await buyPaidSubscription(productID, paymentToken, redirectURL); + break; + case PaymentActionType.Update: + await updateSubscription(productID, paymentToken, redirectURL); + break; + default: + throw Error(CUSTOM_ERROR.INVALID_ACTION); + } + } catch (e: any) { + console.error("Error: ", JSON.stringify(e)); + if (e.message !== CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS) { + logError(e); + } + throw e; + } +} + +async function getUserStripeAccountCountry( + paymentToken: string, +): Promise<{ stripeAccountCountry: StripeAccountCountry }> { + const response = await HTTPService.get( + `${getEndpoint()}/billing/stripe-account-country`, + undefined, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +async function getStripe( + redirectURL: string, + stripeAccount: StripeAccountCountry, +) { + try { + const publishableKey = getStripePublishableKey(stripeAccount); + const stripe = await loadStripe(publishableKey); + + if (!stripe) { + throw Error("stripe load failed"); + } + return stripe; + } catch (e) { + logError(e, "stripe load failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.STRIPE_ERROR, + ); + throw e; + } +} + +export async function buyPaidSubscription( + productID: string, + paymentToken: string, + redirectURL: string, +) { + try { + const { stripeAccountCountry } = + await getUserStripeAccountCountry(paymentToken); + const stripe = await getStripe(redirectURL, stripeAccountCountry); + const { sessionID } = await createCheckoutSession( + productID, + paymentToken, + redirectURL, + ); + await stripe.redirectToCheckout({ + sessionId: sessionID, + }); + } catch (e) { + logError(e, "subscription purchase failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.SERVER_ERROR, + ); + throw e; + } +} + +async function createCheckoutSession( + productID: string, + paymentToken: string, + redirectURL: string, +): Promise<{ sessionID: string }> { + const response = await HTTPService.get( + `${getEndpoint()}/billing/stripe/checkout-session`, + { + productID, + redirectURL, + }, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +export async function updateSubscription( + productID: string, + paymentToken: string, + redirectURL: string, +) { + try { + const { stripeAccountCountry } = + await getUserStripeAccountCountry(paymentToken); + const stripe = await getStripe(redirectURL, stripeAccountCountry); + const { result } = await subscriptionUpdateRequest( + paymentToken, + productID, + ); + switch (result.status) { + case PAYMENT_INTENT_STATUS.SUCCESS: + // subscription updated successfully + // no-op required + return redirectToApp(redirectURL, RESPONSE_STATUS.success); + + case PAYMENT_INTENT_STATUS.REQUIRE_PAYMENT_METHOD: + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.REQUIRE_PAYMENT_METHOD, + ); + case PAYMENT_INTENT_STATUS.REQUIRE_ACTION: { + const { error } = await stripe.confirmCardPayment( + result.clientSecret, + ); + if (error) { + logError( + error, + `${error.message} - subscription update failed`, + ); + if (error.type === STRIPE_ERROR_TYPE.CARD_ERROR) { + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.REQUIRE_PAYMENT_METHOD, + ); + } else if ( + error.type === STRIPE_ERROR_TYPE.AUTHENTICATION_ERROR || + error.code === STRIPE_ERROR_CODE.AUTHENTICATION_ERROR + ) { + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.AUTHENTICATION_FAILED, + ); + } else { + return redirectToApp(redirectURL, RESPONSE_STATUS.fail); + } + } else { + return redirectToApp(redirectURL, RESPONSE_STATUS.success); + } + } + } + } catch (e) { + logError(e, "subscription update failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.SERVER_ERROR, + ); + throw e; + } +} + +async function subscriptionUpdateRequest( + paymentToken: string, + productID: string, +): Promise { + const response = await HTTPService.post( + `${getEndpoint()}/billing/stripe/update-subscription`, + { + productID, + }, + undefined, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +function redirectToApp(redirectURL: string, status: string, reason?: string) { + let completePath = `${redirectURL}?status=${status}`; + if (reason) { + completePath = `${completePath}&reason=${reason}`; + } + window.location.href = completePath; +} diff --git a/web/apps/payments/src/styles/globals.css b/web/apps/payments/src/styles/globals.css new file mode 100644 index 000000000..c1a7f539d --- /dev/null +++ b/web/apps/payments/src/styles/globals.css @@ -0,0 +1,46 @@ +html, +body { + padding: 0; + margin: 0; + font-family: system-ui, sans-serif; + height: 100%; + flex: 1; + display: flex; + flex-direction: column; + background-color: #191919 !important; + color: #aaa !important; +} + +:is(h1, h2, h3, h4, h5, h6) { + color: #d7d7d7; +} + +#__next { + flex: 1; + display: flex; + flex-direction: column; +} + +.container { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.loading-spinner { + color: #28a745; + width: 2rem; + height: 2rem; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: 0.75s linear infinite spinner-border; +} + +@keyframes spinner-border { + 100% { + transform: rotate(360deg); + } +} diff --git a/web/apps/payments/src/utils/error.ts b/web/apps/payments/src/utils/error.ts new file mode 100644 index 000000000..c38b2bf6c --- /dev/null +++ b/web/apps/payments/src/utils/error.ts @@ -0,0 +1,5 @@ +export const CUSTOM_ERROR = { + DIRECT_OPEN_WITH_NO_QUERY_PARAMS: "direct open with no query params", + MISSING_REQUIRED_QUERY_PARAM: "missing required query param", + INVALID_ACTION: "invalid action", +}; diff --git a/web/apps/payments/src/utils/log.ts b/web/apps/payments/src/utils/log.ts new file mode 100644 index 000000000..5a2640113 --- /dev/null +++ b/web/apps/payments/src/utils/log.ts @@ -0,0 +1,3 @@ +export const logError = (e: unknown, msg?: string) => { + console.error(msg, e); +}; diff --git a/web/apps/payments/src/utils/strings.ts b/web/apps/payments/src/utils/strings.ts new file mode 100644 index 000000000..44ba64b8f --- /dev/null +++ b/web/apps/payments/src/utils/strings.ts @@ -0,0 +1,7 @@ +const englishConstants = { + TITLE: "Payments | ente.io", + SOMETHING_WENT_WRONG: "Oops, something went wrong.", + NOT_FOUND: "404 | This page could not be found.", +}; + +export default englishConstants; diff --git a/web/apps/payments/tsconfig.json b/web/apps/payments/tsconfig.json new file mode 100644 index 000000000..d21df2bc4 --- /dev/null +++ b/web/apps/payments/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "baseUrl": "./src", + "incremental": true, + "allowJs": true + }, + "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "next.config.js"] +} diff --git a/web/apps/photos/.env b/web/apps/photos/.env new file mode 100644 index 000000000..2680ead9f --- /dev/null +++ b/web/apps/photos/.env @@ -0,0 +1,87 @@ +# Sample configuration file +# +# All variables are commented out by default. Copy paste this into a new file +# called `.env.local` (or create a new empty file with that name) and add the +# environment variables you want to apply. `.env.local` is gitignored, so you +# can freely customize it for your local setup. +# +# `.env.local` is picked up by Next.js when NODE_ENV is 'development' (it is +# 'production' by default, but gets set to 'development' when we run `next +# dev`). Here's a list of the various files that come into play: +# +# .env loaded in all cases +# .env.local loaded in all cases, gitignored +# .env.development only loaded for yarn dev +# .env.development.local only loaded for yarn dev, gitignored +# .env.production only loaded for yarn build +# .env.production.local only loaded for yarn build, gitignored +# +# Alternatively, these variables can be provided as environment variables, say: +# +# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos +# +# Variables prefixed with NEXT_PUBLIC_ are made available when Next.js runs our +# code in the browser (Behind the scenes, Next.js just hardcodes occurrences of +# `process.env.NEXT_PUBLIC_FOO` with the value of the `NEXT_PUBLIC_FOO` env var +# when the bundle is built). See +# https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables +# +# A development build behaves differently in some aspects: +# +# - Logs go to the browser console (in addition to the log file) +# - There is some additional logging +# - ... (search for isDevBuild to see all impacts) +# +# Note that even in development build, the app still connects to the production +# APIs by default (can be customized using the env vars below). This is usually +# a good default, for example a customer cloning this repository want to build +# and run the client from source but still use their actual Ente account. + +# The Ente API endpoint +# +# NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:3000 + +# The Ente API endpoint for accounts related functionality +# +# NEXT_PUBLIC_ENTE_ACCOUNTS_ENDPOINT = http://localhost:3001 + +# The Ente API endpoint for payments related functionality +# +# NEXT_PUBLIC_ENTE_PAYMENTS_ENDPOINT = http://localhost:3001 + +# The URL for the shared albums deployment +# +# The shared albums are served from the photos app code, and "albums.ente.io" is +# a CNAME alias to the main photo app itself. When the main index page loads, it +# checks to see if the host is "albums.ente.io", and if so, redirects to +# /shared-albums. +# +# This environment variable allows us to check for a host other than +# "albums.ente.io". By setting this to localhost:3002 and running the photos app +# on port 3002 (using `yarn dev:albums`), we can connect to it and emulate the +# production behaviour. +# +# Enhancement: Consider splitting this into a separate app/ in this repository. +# That can also reduce bundle sizes and make it load faster. +# +# NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = http://localhost:3002 + +# The URL of the family plans web app deployment +# +# Currently the source code for the family plan related pages is in a separate +# repository (https://github.com/ente-io/families). The mobile app also uses +# these pages. +# +# Enhancement: Consider moving that into the app/ folder in this repository. +# +# NEXT_PUBLIC_ENTE_FAMILY_ENDPOINT = http://localhost:3001 + +# The JSON which describes the expected results of our integration tests. See +# `upload.test.ts` for more details of the expected format. +# +# This is perhaps easier to specify as an environment variable, since then we +# can directly read from the source file when running `yarn dev`. For example, +# +# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON=`cat path/to/expected.json` yarn dev +# +# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON = {} diff --git a/web/apps/photos/.env.development b/web/apps/photos/.env.development index 548e5bbfb..4def17ed5 100644 --- a/web/apps/photos/.env.development +++ b/web/apps/photos/.env.development @@ -1,79 +1,18 @@ -# Sample configuration file +# Develop against a server running on localhost # -# All variables are commented out by default. Copy paste this into a new file -# called `.env.local` (or create a new file with that name) and add the -# environment variables you want to apply during development. `.env.local` is -# gitignored, so you can freely customize it for your local setup. +# Copy this file to `.env.local` or `.env.development.local` to give you a +# baseline setup. For more details, see `.env`, # -# `.env.local` is picked up by Next.js when NODE_ENV is 'development' (it is -# 'production' by default, but gets set to 'development' when we run `next dev`) -# -# Alternatively, these variables can be provided as environment variables, say: +# Equivalent CLI command using environment variables would be # # NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos # -# Variables prefixed with NEXT_PUBLIC_ are made available when Next.js runs our -# code in the browser (Behind the scenes, Next.js just hardcodes occurrences of -# `process.env.NEXT_PUBLIC_FOO` with the value of the `NEXT_PUBLIC_FOO` env var -# when the bundle is built). See -# https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables -# -# A development build behaves differently in some aspects: -# -# - Logs go to the browser console (in addition to the log file) -# - There is some additional logging -# - ... (search for isDevBuild to see all impacts) -# -# Note that even in development build, the app still connects to the production -# APIs by default (can be customized using the env vars below). This is usually -# a good default, for example a customer cloning this repository want to build -# and run the client from source but still use their actual Ente account. -# The Ente API endpoint -# -# NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:3000 +NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 -# The Ente API endpoint for accounts related functionality +# If you wish to preview how the shared albums work, you can use `yarn +# dev:albums`. The equivalent CLI command using env vars would be # -# NEXT_PUBLIC_ENTE_ACCOUNTS_ENDPOINT = http://localhost:3001 +# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 yarn dev:albums -# The Ente API endpoint for payments related functionality -# -# NEXT_PUBLIC_ENTE_PAYMENT_ENDPOINT = http://localhost:3001 - -# The URL for the shared albums deployment -# -# The shared albums are served from the photos app code, and "albums.ente.io" is -# a CNAME alias to the main photo app itself. When the main index page loads, it -# checks to see if the host is "albums.ente.io", and if so, redirects to -# /shared-albums. -# -# This environment variable allows us to check for a host other than -# "albums.ente.io". By setting this to localhost:3002 and running the photos app -# on port 3002 (using `yarn dev:albums`), we can connect to it and emulate the -# production behaviour. -# -# Enhancement: Consider splitting this into a separate app/ in this repository. -# That can also reduce bundle sizes and make it load faster. -# -# NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = http://localhost:3002 - -# The URL of the family plans web app deployment -# -# Currently the source code for the family plan related pages is in a separate -# repository (https://github.com/ente-io/families). The mobile app also uses -# these pages. -# -# Enhancement: Consider moving that into the app/ folder in this repository. -# -# NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT = http://localhost:3003 - -# The JSON which describes the expected results of our integration tests. See -# `upload.test.ts` for more details of the expected format. -# -# This is perhaps easier to specify as an environment variable, since then we -# can directly read from the source file when running `yarn dev`. For example, -# -# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON=`cat path/to/expected.json` yarn dev -# -# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON = {} +NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = http://localhost:3002 diff --git a/web/apps/photos/.env.localhost b/web/apps/photos/.env.localhost deleted file mode 100644 index 136d0118b..000000000 --- a/web/apps/photos/.env.localhost +++ /dev/null @@ -1,20 +0,0 @@ -# Develop against a server running on localhost -# -# Copy this file to `.env`. Then if you run a local instance of the web client -# with `yarn dev:photos`, it will connect to a locally running instance of the -# server. Not everything will work, you might need to set other env vars (see -# `.env.development`), but it should give you a usable baseline setup. -# -# Equivalent CLI command using environment variables would be -# -# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos -# - -NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 - -# If you wish to preview how the shared albums work, you can use `yarn -# dev:albums`. The equivalent CLI command using env vars would be -# -# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 yarn dev:albums - -NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = http://localhost:3002 diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 1a088c99d..deac7ad04 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -16,10 +16,8 @@ "@tensorflow/tfjs-converter": "^4.10.0", "@tensorflow/tfjs-core": "^4.10.0", "@tensorflow/tfjs-tflite": "0.0.1-alpha.7", - "@zip.js/zip.js": "2.4.2", "bip39": "^3.0.4", "blazeface-back": "^0.0.9", - "bootstrap": "^4.5.2", "bs58": "^5.0.0", "chrono-node": "^2.2.6", "comlink": "^4.3.0", @@ -45,7 +43,7 @@ "p-queue": "^7.1.0", "photoswipe": "file:./thirdparty/photoswipe", "piexifjs": "^1.0.6", - "react-bootstrap": "^1.3.0", + "pure-react-carousel": "^1.30.1", "react-datepicker": "^4.16.0", "react-dropzone": "^11.2.4", "react-otp-input": "^2.3.1", @@ -56,7 +54,7 @@ "sanitize-filename": "^1.6.3", "similarity-transformation": "^0.0.1", "transformation-matrix": "^2.15.0", - "uuid": "^9.0.0", + "uuid": "^9.0.1", "vscode-uri": "^3.0.7", "xml-js": "^1.6.11", "zxcvbn": "^4.4.2" diff --git a/web/apps/photos/public/locales/bg-BG/translation.json b/web/apps/photos/public/locales/bg-BG/translation.json index 6b02fae65..03faf16c2 100644 --- a/web/apps/photos/public/locales/bg-BG/translation.json +++ b/web/apps/photos/public/locales/bg-BG/translation.json @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/de-DE/translation.json b/web/apps/photos/public/locales/de-DE/translation.json index a515e7366..1d79b5bb7 100644 --- a/web/apps/photos/public/locales/de-DE/translation.json +++ b/web/apps/photos/public/locales/de-DE/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Anmelden", "SIGN_UP": "Registrieren", - "NEW_USER": "Neu bei ente", + "NEW_USER": "Neu bei Ente", "EXISTING_USER": "Existierender Benutzer", "ENTER_NAME": "Name eingeben", "PUBLIC_UPLOADER_NAME_MESSAGE": "Füge einen Namen hinzu, damit deine Freunde wissen, wem sie für diese tollen Fotos zu danken haben!", @@ -163,7 +163,7 @@ "SUBSCRIPTION_PURCHASE_CANCELLED": "Dein Kauf wurde abgebrochen. Bitte versuche es erneut, wenn du abonnieren willst", "SUBSCRIPTION_PURCHASE_FAILED": "Kauf des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", "SUBSCRIPTION_UPDATE_FAILED": "Aktualisierung des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", - "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Es tut uns leid, die Zahlung ist fehlgeschlagen, als wir versuchten Ihre Karte zu belasten. Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut", "STRIPE_AUTHENTICATION_FAILED": "Wir können deine Zahlungsmethode nicht authentifizieren. Bitte wähle eine andere Zahlungsmethode und versuche es erneut", "UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren", "MONTHLY": "Monatlich", @@ -260,13 +260,13 @@ "ENABLE": "Aktivieren", "LOST_DEVICE": "Zwei-Faktor-Gerät verloren", "INCORRECT_CODE": "Falscher Code", - "TWO_FACTOR_INFO": "", + "TWO_FACTOR_INFO": "Fügen Sie eine zusätzliche Sicherheitsebene hinzu, indem Sie mehr als Ihre E-Mail und Ihr Passwort benötigen, um sich mit Ihrem Account anzumelden", "DISABLE_TWO_FACTOR_LABEL": "Deaktiviere die Zwei-Faktor-Authentifizierung", "UPDATE_TWO_FACTOR_LABEL": "Authentifizierungsgerät aktualisieren", "DISABLE": "Deaktivieren", "RECONFIGURE": "Neu einrichten", "UPDATE_TWO_FACTOR": "Zweiten Faktor aktualisieren", - "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE_TWO_FACTOR_MESSAGE": "Fahren Sie fort, werden alle Ihre zuvor konfigurierten Authentifikatoren ungültig", "UPDATE": "Aktualisierung", "DISABLE_TWO_FACTOR": "Zweiten Faktor deaktivieren", "DISABLE_TWO_FACTOR_MESSAGE": "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst", @@ -278,24 +278,24 @@ "LAST_EXPORT_TIME": "Letztes Exportdatum", "EXPORT_AGAIN": "Neusynchronisation", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert Ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", "SEND_OTT": "OTP senden", "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", "ETAGS_BLOCKED": "", "SKIPPED_VIDEOS_INFO": "", "LIVE_PHOTOS_DETECTED": "", - "RETRY_FAILED": "", + "RETRY_FAILED": "Fehlgeschlagene Uploads erneut probieren", "FAILED_UPLOADS": "Fehlgeschlagene Uploads ", "SKIPPED_FILES": "Ignorierte Uploads", "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Das Vorschaubild konnte nicht erzeugt werden", "UNSUPPORTED_FILES": "Nicht unterstützte Dateien", - "SUCCESSFUL_UPLOADS": "", + "SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads", "SKIPPED_INFO": "", - "UNSUPPORTED_INFO": "ente unterstützt diese Dateiformate noch nicht", + "UNSUPPORTED_INFO": "Ente unterstützt diese Dateiformate noch nicht", "BLOCKED_UPLOADS": "Blockierte Uploads", "SKIPPED_VIDEOS": "Übersprungene Videos", "INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung", - "INPROGRESS_UPLOADS": "", + "INPROGRESS_UPLOADS": "Upload läuft", "TOO_LARGE_UPLOADS": "Große Dateien", "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Zu wenig Speicher", "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie die maximale Größe für Ihren Speicherplan überschreiten", @@ -338,45 +338,45 @@ "SORT_BY_CREATION_TIME_ASCENDING": "Ältestem", "SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert", "SORT_BY_NAME": "Name", - "COMPRESS_THUMBNAILS": "", - "THUMBNAIL_REPLACED": "", + "COMPRESS_THUMBNAILS": "Vorschaubilder komprimieren", + "THUMBNAIL_REPLACED": "Vorschaubilder komprimiert", "FIX_THUMBNAIL": "Komprimiere", "FIX_THUMBNAIL_LATER": "Später komprimieren", "REPLACE_THUMBNAIL_NOT_STARTED": "", "REPLACE_THUMBNAIL_COMPLETED": "", "REPLACE_THUMBNAIL_NOOP": "", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", - "FIX_CREATION_TIME": "", - "FIX_CREATION_TIME_IN_PROGRESS": "", - "CREATION_TIME_UPDATED": "", - "UPDATE_CREATION_TIME_NOT_STARTED": "", + "FIX_CREATION_TIME": "Zeit reparieren", + "FIX_CREATION_TIME_IN_PROGRESS": "Zeit wird repariert", + "CREATION_TIME_UPDATED": "Datei-Zeit aktualisiert", + "UPDATE_CREATION_TIME_NOT_STARTED": "Wählen Sie die Option, die Sie verwenden möchten", "UPDATE_CREATION_TIME_COMPLETED": "", "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", - "CAPTION_CHARACTER_LIMIT": "", + "CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen", "DATE_TIME_ORIGINAL": "", "DATE_TIME_DIGITIZED": "", "METADATA_DATE": "", "CUSTOM_TIME": "Benutzerdefinierte Zeit", "REOPEN_PLAN_SELECTOR_MODAL": "", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Fehler beim Öffnen der Pläne", "INSTALL": "Installieren", "SHARING_DETAILS": "Details teilen", "MODIFY_SHARING": "Freigabe ändern", "ADD_COLLABORATORS": "Bearbeiter hinzufügen", "ADD_NEW_EMAIL": "Neue E-Mail-Adresse hinzufügen", - "shared_with_people_zero": "", + "shared_with_people_zero": "Mit bestimmten Personen teilen", "shared_with_people_one": "Geteilt mit einer Person", - "shared_with_people_other": "", + "shared_with_people_other": "Geteilt mit {{count, number}} Personen", "participants_zero": "Keine Teilnehmer", "participants_one": "1 Teilnehmer", - "participants_other": "", + "participants_other": "{{count, number}} Teilnehmer", "ADD_VIEWERS": "Betrachter hinzufügen", "PARTICIPANTS": "Teilnehmer", "CHANGE_PERMISSIONS_TO_VIEWER": "", "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", "CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern", "CONVERT_TO_COLLABORATOR": "", - "CHANGE_PERMISSION": "", + "CHANGE_PERMISSION": "Berechtigung ändern?", "REMOVE_PARTICIPANT": "Entfernen?", "CONFIRM_REMOVE": "Ja, entfernen", "MANAGE": "Verwalten", @@ -384,9 +384,9 @@ "COLLABORATOR_RIGHTS": "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen", "REMOVE_PARTICIPANT_HEAD": "Teilnehmer entfernen", "OWNER": "Besitzer", - "COLLABORATORS": "", + "COLLABORATORS": "Bearbeiter", "ADD_MORE": "Mehr hinzufügen", - "VIEWERS": "", + "VIEWERS": "Zuschauer", "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - Nicht gefunden", @@ -401,19 +401,19 @@ "NO_DEVICE_LIMIT": "Keins", "LINK_EXPIRY": "Ablaufdatum des Links", "NEVER": "Niemals", - "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD": "Download deaktivieren", "DISABLE_FILE_DOWNLOAD_MESSAGE": "", - "MALICIOUS_CONTENT": "", - "COPYRIGHT": "", - "SHARED_USING": "", + "MALICIOUS_CONTENT": "Enthält schädliche Inhalte", + "COPYRIGHT": "Verletzung des Urheberrechts von jemandem, den ich repräsentieren darf", + "SHARED_USING": "Freigegeben über ", "ENTE_IO": "ente.io", "SHARING_REFERRAL_CODE": "", "LIVE": "LIVE", - "DISABLE_PASSWORD": "", - "DISABLE_PASSWORD_MESSAGE": "", + "DISABLE_PASSWORD": "Passwort-Sperre deaktivieren", + "DISABLE_PASSWORD_MESSAGE": "Sind Sie sicher, dass Sie die Passwort-Sperre deaktivieren möchten?", "PASSWORD_LOCK": "Passwort Sperre", "LOCK": "Sperren", - "DOWNLOAD_UPLOAD_LOGS": "", + "DOWNLOAD_UPLOAD_LOGS": "Debug-Logs", "UPLOAD_FILES": "Datei", "UPLOAD_DIRS": "Ordner", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", @@ -421,7 +421,7 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "Du hast keine Duplikate, die gelöscht werden können", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "Dateien", + "FILES": "dateien", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", @@ -467,13 +467,13 @@ "FAMILY_PLAN": "Familientarif", "DOWNLOAD_LOGS": "Logs herunterladen", "DOWNLOAD_LOGS_MESSAGE": "", - "CHANGE_FOLDER": "", - "TWO_MONTHS_FREE": "", + "CHANGE_FOLDER": "Ordner ändern", + "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", "GB": "GB", "POPULAR": "Beliebt", - "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren", "FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr", - "CURRENT_USAGE": "", + "CURRENT_USAGE": "Aktuelle Nutzung ist {{usage}}", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/en-US/translation.json b/web/apps/photos/public/locales/en-US/translation.json index ea74f4f5f..c0e5cd711 100644 --- a/web/apps/photos/public/locales/en-US/translation.json +++ b/web/apps/photos/public/locales/en-US/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Login", "SIGN_UP": "Signup", - "NEW_USER": "New to ente", + "NEW_USER": "New to Ente", "EXISTING_USER": "Existing user", "ENTER_NAME": "Enter name", "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Delete files?", "TRASH_FILE_TITLE": "Delete file?", "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", + "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your Ente account.", "DELETE": "Delete", "DELETE_OPTION": "Delete (DEL)", "FAVORITE_OPTION": "Favorite (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", + "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets Ente's encryption standards, please try using the mobile app or another browser", "CHANGE_PASSWORD": "Change password", "GO_BACK": "Go back", "RECOVERY_KEY": "Recovery key", @@ -278,11 +278,11 @@ "LAST_EXPORT_TIME": "Last export time", "EXPORT_AGAIN": "Resync", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking Ente from saving data into local storage. please try loading this page after switching your browsing mode.", "SEND_OTT": "Send OTP", "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", + "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing Ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", + "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for Ente and share with the intended recipients using their email.

", "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", "RETRY_FAILED": "Retry failed uploads", "FAILED_UPLOADS": "Failed uploads ", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Unsupported files", "SUCCESSFUL_UPLOADS": "Successful uploads", "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", + "UNSUPPORTED_INFO": "Ente does not support these file formats yet", "BLOCKED_UPLOADS": "Blocked uploads", "SKIPPED_VIDEOS": "Skipped videos", "INPROGRESS_METADATA_EXTRACTION": "In progress", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restore to album", "EMPTY_TRASH": "Empty trash", "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", + "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your Ente account.", "LEAVE_SHARED_ALBUM": "Yes, leave", "LEAVE_ALBUM": "Leave album", "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails compressed", "FIX_THUMBNAIL": "Compress", "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like Ente to compress them?", "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", + "FILES": "files", + "EACH": "each", "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", "STOP_UPLOADS_HEADER": "Stop uploads?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Watched folders", "NO_FOLDERS_ADDED": "No folders added yet!", "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", + "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from Ente", "ADD_FOLDER": "Add folder", "STOP_WATCHING": "Stop watching", "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", + "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but Ente will stop automatically updating the linked Ente album on changes in this folder.", "YES_STOP": "Yes, stop", "MONTH_SHORT": "mo", "YEAR": "year", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Continue with free trial", "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", "CURRENT_USAGE": "Current usage is {{usage}}", - "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.", - "DRAG_AND_DROP_HINT": "Or drag and drop into the ente window", + "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to Ente on your computer, or download the Ente mobile/desktop app.", + "DRAG_AND_DROP_HINT": "Or drag and drop into the Ente window", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", "AUTHENTICATE": "Authenticate", "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", "NEVERMIND": "Nevermind", "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", + "UPDATE_INSTALLABLE_MESSAGE": "A new version of Ente is ready to be installed.", "INSTALL_NOW": "Install now", "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", + "UPDATE_AVAILABLE_MESSAGE": "A new version of Ente has been released, but it cannot be automatically downloaded and installed.", "DOWNLOAD_AND_INSTALL": "Download and install", "IGNORE_THIS_VERSION": "Ignore this version", "TODAY": "Today", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "More details", "ENABLE_FACE_SEARCH": "Enable face recognition", "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", "DISABLE_BETA": "Pause recognition", "DISABLE_FACE_SEARCH": "Disable face recognition", "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", + "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow Ente to process face geometry", "LABS": "Labs", "YOURS": "yours", "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Live Photo", "CONVERT": "Convert", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to Ente to persist your changes.", "BRIGHTNESS": "Brightness", "CONTRAST": "Contrast", "SATURATION": "Saturation", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Flip Vertically", "FLIP_HORIZONTALLY": "Flip Horizontally", "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", + "SAVE_A_COPY_TO_ENTE": "Save a copy to Ente", "RESTORE_ORIGINAL": "Restore Original", "TRANSFORM": "Transform", "COLORS": "Colors", @@ -631,11 +631,24 @@ "PAIR_WITH_PIN": "Pair with PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", - "VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.", + "VISIT_CAST_ENTE_IO": "Visit {{url}} on the device you want to pair.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", "CACHE_DIRECTORY": "Cache folder", - "PASSKEYS": "Passkeys", "FREEHAND": "Freehand", "APPLY_CROP": "Apply Crop", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.", + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Delete passkey", + "DELETE_PASSKEY_CONFIRMATION": "Are you sure you want to delete this passkey? This action is irreversible.", + "RENAME_PASSKEY": "Rename passkey", + "ADD_PASSKEY": "Add passkey", + "ENTER_PASSKEY_NAME": "Enter passkey name", + "PASSKEYS_DESCRIPTION": "Passkeys are a modern and secure second-factor for your Ente account. They use on-device biometric authentication for convenience and security.", + "CREATED_AT": "Created at", + "PASSKEY_LOGIN_FAILED": "Passkey login failed", + "PASSKEY_LOGIN_URL_INVALID": "The login URL is invalid.", + "PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.", + "TRY_AGAIN": "Try again", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.", + "LOGIN_WITH_PASSKEY": "Login with passkey" } diff --git a/web/apps/photos/public/locales/es-ES/translation.json b/web/apps/photos/public/locales/es-ES/translation.json index 64c2849eb..ca288c692 100644 --- a/web/apps/photos/public/locales/es-ES/translation.json +++ b/web/apps/photos/public/locales/es-ES/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, web, computadora", "LOGIN": "Conectar", "SIGN_UP": "Registro", - "NEW_USER": "Nuevo en ente", + "NEW_USER": "Nuevo en Ente", "EXISTING_USER": "Usuario existente", "ENTER_NAME": "Introducir nombre", "PUBLIC_UPLOADER_NAME_MESSAGE": "¡Añade un nombre para que tus amigos sepan a quién dar las gracias por estas fotos geniales!", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Autenticación", "NO_DUPLICATES_FOUND": "No tienes archivos duplicados que puedan ser borrados", "CLUB_BY_CAPTURE_TIME": "Club por tiempo de captura", - "FILES": "Archivos", - "EACH": "Cada", + "FILES": "archivos", + "EACH": "cada", "DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados", "STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?", "STOP_UPLOADS_HEADER": "Detener las subidas?", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/fa-IR/translation.json b/web/apps/photos/public/locales/fa-IR/translation.json index 68e1da3b9..2d21fcb3d 100644 --- a/web/apps/photos/public/locales/fa-IR/translation.json +++ b/web/apps/photos/public/locales/fa-IR/translation.json @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/fi-FI/translation.json b/web/apps/photos/public/locales/fi-FI/translation.json index fbc0b937a..888ed7093 100644 --- a/web/apps/photos/public/locales/fi-FI/translation.json +++ b/web/apps/photos/public/locales/fi-FI/translation.json @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/fr-FR/translation.json b/web/apps/photos/public/locales/fr-FR/translation.json index 2d183c9eb..c6d719e02 100644 --- a/web/apps/photos/public/locales/fr-FR/translation.json +++ b/web/apps/photos/public/locales/fr-FR/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Ordinateur", "LOGIN": "Connexion", "SIGN_UP": "Inscription", - "NEW_USER": "Nouveau sur ente", + "NEW_USER": "Nouveau sur Ente", "EXISTING_USER": "Utilisateur existant", "ENTER_NAME": "Saisir un nom", "PUBLIC_UPLOADER_NAME_MESSAGE": "Ajouter un nom afin que vos amis sachent qui remercier pour ces magnifiques photos!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Supprimer les fichiers ?", "TRASH_FILE_TITLE": "Supprimer le fichier ?", "DELETE_FILES_TITLE": "Supprimer immédiatement?", - "DELETE_FILES_MESSAGE": "Les fichiers sélectionnés seront définitivement supprimés de votre compte ente.", + "DELETE_FILES_MESSAGE": "Les fichiers sélectionnés seront définitivement supprimés de votre compte Ente.", "DELETE": "Supprimer", "DELETE_OPTION": "Supprimer (DEL)", "FAVORITE_OPTION": "Favori (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Albums séparés", "SESSION_EXPIRED_MESSAGE": "Votre session a expiré, veuillez vous reconnecter pour poursuivre", "SESSION_EXPIRED": "Session expiré", - "PASSWORD_GENERATION_FAILED": "Votre navigateur ne permet pas de générer une clé forte correspondant aux standards de chiffrement de ente, veuillez réessayer en utilisant l'appli mobile ou un autre navigateur", + "PASSWORD_GENERATION_FAILED": "Votre navigateur ne permet pas de générer une clé forte correspondant aux standards de chiffrement de Ente, veuillez réessayer en utilisant l'appli mobile ou un autre navigateur", "CHANGE_PASSWORD": "Modifier le mot de passe", "GO_BACK": "Retour", "RECOVERY_KEY": "Clé de récupération", @@ -278,10 +278,10 @@ "LAST_EXPORT_TIME": "Horaire du dernier export", "EXPORT_AGAIN": "Resynchro", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Stockage local non accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Votre navigateur ou un complément bloque ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Votre navigateur ou un complément bloque Ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.", "SEND_OTT": "Envoyer l'OTP", "EMAIl_ALREADY_OWNED": "Cet e-mail est déjà pris", - "ETAGS_BLOCKED": "

Nosu n'avons pas pu charger les fichiers suivants à cause de la configuration de votre navigateur.

Veuillez désactiver tous les compléments qui pourraient empêcher ente d'utiliser les eTags pour charger de larges fichiers, ou bien utilisez notre appli pour ordinateurpour une meilleure expérience lors des chargements.

", + "ETAGS_BLOCKED": "

Nosu n'avons pas pu charger les fichiers suivants à cause de la configuration de votre navigateur.

Veuillez désactiver tous les compléments qui pourraient empêcher Ente d'utiliser les eTags pour charger de larges fichiers, ou bien utilisez notre appli pour ordinateurpour une meilleure expérience lors des chargements.

", "SKIPPED_VIDEOS_INFO": "

Actuellement, nous ne supportons pas l'ajout de videos via des liens publics.

Pour partager des vidéos, veuillez vous connecter àente et partager en utilisant l'e-mail concerné.

", "LIVE_PHOTOS_DETECTED": "Les fichiers photos et vidéos depuis votre espace Live Photos ont été fusionnés en un seul fichier", "RETRY_FAILED": "Réessayer les chargements ayant échoués", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Fichiers non supportés", "SUCCESSFUL_UPLOADS": "Chargements réussis", "SKIPPED_INFO": "Ignorés car il y a des fichiers avec des noms identiques dans le même album", - "UNSUPPORTED_INFO": "ente ne supporte pas encore ces formats de fichiers", + "UNSUPPORTED_INFO": "Ente ne supporte pas encore ces formats de fichiers", "BLOCKED_UPLOADS": "Chargements bloqués", "SKIPPED_VIDEOS": "Vidéos ignorées", "INPROGRESS_METADATA_EXTRACTION": "En cours", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restaurer vers l'album", "EMPTY_TRASH": "Corbeille vide", "EMPTY_TRASH_TITLE": "Vider la corbeille ?", - "EMPTY_TRASH_MESSAGE": "Ces fichiers seront définitivement supprimés de votre compte ente.", + "EMPTY_TRASH_MESSAGE": "Ces fichiers seront définitivement supprimés de votre compte Ente.", "LEAVE_SHARED_ALBUM": "Oui, quitter", "LEAVE_ALBUM": "Quitter l'album", "LEAVE_SHARED_ALBUM_TITLE": "Quitter l'album partagé?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Les miniatures sont compressées", "FIX_THUMBNAIL": "Compresser", "FIX_THUMBNAIL_LATER": "Compresser plus tard", - "REPLACE_THUMBNAIL_NOT_STARTED": "Certaines miniatures de vidéos peuvent être compressées pour gagner de la place. Voulez-vous que ente les compresse?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Certaines miniatures de vidéos peuvent être compressées pour gagner de la place. Voulez-vous que Ente les compresse?", "REPLACE_THUMBNAIL_COMPLETED": "Toutes les miniatures ont été compressées", "REPLACE_THUMBNAIL_NOOP": "Vous n'avez aucune miniature qui peut être encore plus compressée", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Impossible de compresser certaines miniatures, veuillez réessayer", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authentificateur", "NO_DUPLICATES_FOUND": "Vous n'avez aucun fichier dédupliqué pouvant être nettoyé", "CLUB_BY_CAPTURE_TIME": "Durée de la capture par club", - "FILES": "Fichiers", - "EACH": "Chacun", + "FILES": "fichiers", + "EACH": "chacun", "DEDUPLICATE_BASED_ON_SIZE": "Les fichiers suivants ont été clubbed, basé sur leurs tailles, veuillez corriger et supprimer les objets que vous pensez être dupliqués", "STOP_ALL_UPLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?", "STOP_UPLOADS_HEADER": "Arrêter les chargements ?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Voir les dossiers", "NO_FOLDERS_ADDED": "Aucun dossiers d'ajouté!", "FOLDERS_AUTOMATICALLY_MONITORED": "Les dossiers que vous ajoutez ici seront supervisés automatiquement", - "UPLOAD_NEW_FILES_TO_ENTE": "Charger de nouveaux fichiers sur ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Retirer de ente les fichiers supprimés", + "UPLOAD_NEW_FILES_TO_ENTE": "Charger de nouveaux fichiers sur Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Retirer de Ente les fichiers supprimés", "ADD_FOLDER": "Ajouter un dossier", "STOP_WATCHING": "Arrêter de voir", "STOP_WATCHING_FOLDER": "Arrêter de voir le dossier?", - "STOP_WATCHING_DIALOG_MESSAGE": "Vos fichiers existants ne seront pas supprimés, mais ente arrêtera automatiquement de mettre à jour le lien de l'album à chaque changements sur ce dossier.", + "STOP_WATCHING_DIALOG_MESSAGE": "Vos fichiers existants ne seront pas supprimés, mais Ente arrêtera automatiquement de mettre à jour le lien de l'album à chaque changements sur ce dossier.", "YES_STOP": "Oui, arrêter", "MONTH_SHORT": "mo", "YEAR": "année", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite", "FREE_PLAN_DESCRIPTION": "1 Go pour 1 an", "CURRENT_USAGE": "L'utilisation actuelle est de {{usage}}", - "WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à ente sur votre ordinateur, ou télécharger l'appli ente mobile/ordinateur.", - "DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre ente", + "WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à Ente sur votre ordinateur, ou télécharger l'appli Ente mobile/ordinateur.", + "DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre Ente", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "

Vos données chargées seront programmées pour suppression, et votre comptre sera supprimé définitivement .

Cette action n'est pas reversible.

", "AUTHENTICATE": "Authentification", "UPLOADED_TO_SINGLE_COLLECTION": "Chargé dans une seule collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Chargé dans des collections séparées", "NEVERMIND": "Peu-importe", "UPDATE_AVAILABLE": "Une mise à jour est disponible", - "UPDATE_INSTALLABLE_MESSAGE": "Une nouvelle version de ente est prête à être installée.", + "UPDATE_INSTALLABLE_MESSAGE": "Une nouvelle version de Ente est prête à être installée.", "INSTALL_NOW": "Installer maintenant", "INSTALL_ON_NEXT_LAUNCH": "Installer au prochain démarrage", - "UPDATE_AVAILABLE_MESSAGE": "Une nouvelle version de ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", + "UPDATE_AVAILABLE_MESSAGE": "Une nouvelle version de Ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", "DOWNLOAD_AND_INSTALL": "Télécharger et installer", "IGNORE_THIS_VERSION": "Ignorer cette version", "TODAY": "Aujourd'hui", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "Plus de détails", "ENABLE_FACE_SEARCH": "Activer la recherche faciale", "ENABLE_FACE_SEARCH_TITLE": "Activer la recherche faciale ?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face search, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face search, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

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

", "DISABLE_BETA": "Désactiver la bêta", "DISABLE_FACE_SEARCH": "Désactiver la recherche faciale", "DISABLE_FACE_SEARCH_TITLE": "Désactiver la recherche faciale ?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente will stop processing face geometry, and will also disable ML search (beta)

You can reenable face search again if you wish, so this operation is safe

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry, and will also disable ML search (beta)

You can reenable face search again if you wish, so this operation is safe

", "ADVANCED": "Avancé", - "FACE_SEARCH_CONFIRMATION": "Je comprends, et je souhaite permettre à ente de traiter la géométrie faciale", + "FACE_SEARCH_CONFIRMATION": "Je comprends, et je souhaite permettre à Ente de traiter la géométrie faciale", "LABS": "Labs", "YOURS": "Le vôtre", "PASSPHRASE_STRENGTH_WEAK": "Sécurité du mot de passe : faible", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Photos en direct", "CONVERT": "Convertir", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Êtes-vous sûr de vouloir fermer l'éditeur ?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Téléchargez votre image modifiée ou enregistrez une copie sur ente pour maintenir vos modifications.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Téléchargez votre image modifiée ou enregistrez une copie sur Ente pour maintenir vos modifications.", "BRIGHTNESS": "Luminosité", "CONTRAST": "Contraste", "SATURATION": "Saturation", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Basculer verticalement", "FLIP_HORIZONTALLY": "Retourner horizontalement", "DOWNLOAD_EDITED": "Téléchargement modifié", - "SAVE_A_COPY_TO_ENTE": "Enregistrer une copie dans ente", + "SAVE_A_COPY_TO_ENTE": "Enregistrer une copie dans Ente", "RESTORE_ORIGINAL": "Restaurer l'original", "TRANSFORM": "Transformer", "COLORS": "Couleurs", @@ -631,11 +631,24 @@ "PAIR_WITH_PIN": "Associer avec le code PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Choisissez un périphérique compatible avec la caste à partir de la fenêtre pop-up du navigateur.", "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "L'association avec le code PIN fonctionne pour tout appareil grand écran sur lequel vous voulez lire votre album.", - "VISIT_CAST_ENTE_IO": "Visitez cast.ente.io sur l'appareil que vous voulez associer.", + "VISIT_CAST_ENTE_IO": "Visitez {{url}} sur l'appareil que vous voulez associer.", "CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.", "CACHE_DIRECTORY": "Dossier du cache", - "PASSKEYS": "Clés d'accès", "FREEHAND": "Main levée", "APPLY_CROP": "Appliquer le recadrage", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.", + "PASSKEYS": "Clés d'accès", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/it-IT/translation.json b/web/apps/photos/public/locales/it-IT/translation.json index dd7afc158..2989ba4ce 100644 --- a/web/apps/photos/public/locales/it-IT/translation.json +++ b/web/apps/photos/public/locales/it-IT/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Accedi", "SIGN_UP": "Registrati", - "NEW_USER": "Nuovo utente", + "NEW_USER": "", "EXISTING_USER": "Accedi", "ENTER_NAME": "Inserisci il nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Aggiungi un nome in modo che i tuoi amici sappiano chi ringraziare per queste fantastiche foto!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Elimina file?", "TRASH_FILE_TITLE": "Eliminare il file?", "DELETE_FILES_TITLE": "Eliminare immediatamente?", - "DELETE_FILES_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "DELETE_FILES_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account Ente.", "DELETE": "Cancella", "DELETE_OPTION": "Cancella (DEL)", "FAVORITE_OPTION": "Preferito (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Album separati", "SESSION_EXPIRED_MESSAGE": "La sessione è scaduta. Per continuare, esegui nuovamente l'accesso", "SESSION_EXPIRED": "Sessione scaduta", - "PASSWORD_GENERATION_FAILED": "Il tuo browser non è stato in grado di generare una chiave forte che soddisfa gli standard di crittografia ente, prova ad usare l'app per dispositivi mobili o un altro browser", + "PASSWORD_GENERATION_FAILED": "Il tuo browser non è stato in grado di generare una chiave forte che soddisfa gli standard di crittografia Ente, prova ad usare l'app per dispositivi mobili o un altro browser", "CHANGE_PASSWORD": "Cambia password", "GO_BACK": "Torna indietro", "RECOVERY_KEY": "Chiave di recupero", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Ripristina nell'album", "EMPTY_TRASH": "Svuota il cestino", "EMPTY_TRASH_TITLE": "Vuoi svuotare il cestino?", - "EMPTY_TRASH_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "EMPTY_TRASH_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account Ente.", "LEAVE_SHARED_ALBUM": "Sì, esci", "LEAVE_ALBUM": "Abbandona l'album", "LEAVE_SHARED_ALBUM_TITLE": "Abbandonare l'album condiviso?", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/ko-KR/translation.json b/web/apps/photos/public/locales/ko-KR/translation.json index 7482dd397..9454a0356 100644 --- a/web/apps/photos/public/locales/ko-KR/translation.json +++ b/web/apps/photos/public/locales/ko-KR/translation.json @@ -1,14 +1,14 @@ { - "HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요", - "HERO_SLIDE_1": "종단간 암호화가 기본지원입니다", - "HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다", - "HERO_SLIDE_2": "오랫동안 보존할 수 있도록한 설계", - "HERO_SLIDE_3_TITLE": "
어디에서나
이용가능
", + "HERO_SLIDE_1_TITLE": "
당신의 추억을 위한
비공개 백업
", + "HERO_SLIDE_1": "종단간 암호화를 기본적으로 지원합니다", + "HERO_SLIDE_2_TITLE": "
낙진 대피소에
안전하게 보관됨
", + "HERO_SLIDE_2": "장기 보존을 위해 설계되었습니다", + "HERO_SLIDE_3_TITLE": "
모든 기기에서
사용 가능
", "HERO_SLIDE_3": "안드로이드, iOS, 웹, 데스크탑", "LOGIN": "로그인", "SIGN_UP": "회원가입", - "NEW_USER": "ente의 새소식", - "EXISTING_USER": "기존 사용자", + "NEW_USER": "Ente 의 새소식", + "EXISTING_USER": "기존 회원 로그인", "ENTER_NAME": "이름 입력", "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", "ENTER_EMAIL": "이메일 주소를 입력하세요", @@ -76,42 +76,42 @@ "DOWNLOAD": "다운로드", "DOWNLOAD_OPTION": "다운로드 (D)", "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", - "DOWNLOAD_UNCATEGORIZED": "", - "DOWNLOAD_HIDDEN_ITEMS": "", - "COPY_OPTION": "", - "TOGGLE_FULLSCREEN": "", - "ZOOM_IN_OUT": "", - "PREVIOUS": "", - "NEXT": "", - "TITLE_PHOTOS": "", - "TITLE_ALBUMS": "", - "TITLE_AUTH": "", - "UPLOAD_FIRST_PHOTO": "", - "IMPORT_YOUR_FOLDERS": "", - "UPLOAD_DROPZONE_MESSAGE": "", - "WATCH_FOLDER_DROPZONE_MESSAGE": "", - "TRASH_FILES_TITLE": "", - "TRASH_FILE_TITLE": "", - "DELETE_FILES_TITLE": "", - "DELETE_FILES_MESSAGE": "", - "DELETE": "", - "DELETE_OPTION": "", - "FAVORITE_OPTION": "", - "UNFAVORITE_OPTION": "", - "MULTI_FOLDER_UPLOAD": "", - "UPLOAD_STRATEGY_CHOICE": "", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", - "OR": "", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", - "SESSION_EXPIRED_MESSAGE": "", - "SESSION_EXPIRED": "", - "PASSWORD_GENERATION_FAILED": "", - "CHANGE_PASSWORD": "", - "GO_BACK": "", - "RECOVERY_KEY": "", - "SAVE_LATER": "", - "SAVE": "", - "RECOVERY_KEY_DESCRIPTION": "", + "DOWNLOAD_UNCATEGORIZED": "미분류 항목 다운로드", + "DOWNLOAD_HIDDEN_ITEMS": "숨겨진 항목 다운로드", + "COPY_OPTION": "PNG로 복사 (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "전체 화면 열기 (F)", + "ZOOM_IN_OUT": "확대/축소", + "PREVIOUS": "이전 항목 (←)", + "NEXT": "다음 항목 (→)", + "TITLE_PHOTOS": "Ente Photos", + "TITLE_ALBUMS": "Ente Photos", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "첫 번째 사진을 업로드하세요", + "IMPORT_YOUR_FOLDERS": "폴더 가져오기", + "UPLOAD_DROPZONE_MESSAGE": "백업하려는 파일을 올려놓기", + "WATCH_FOLDER_DROPZONE_MESSAGE": "추가하려는 감시 폴더 올려놓기", + "TRASH_FILES_TITLE": "파일들을 삭제할까요?", + "TRASH_FILE_TITLE": "파일을 삭제할까요?", + "DELETE_FILES_TITLE": "즉시 삭제할까요?", + "DELETE_FILES_MESSAGE": "선택된 파일들은 당신의 Ente계정에서 영구적으로 삭제됩니다.", + "DELETE": "삭제", + "DELETE_OPTION": "삭제 (DEL)", + "FAVORITE_OPTION": "즐겨찾기 (L)", + "UNFAVORITE_OPTION": "즐겨찾기해제 (L)", + "MULTI_FOLDER_UPLOAD": "여러 폴더들이 탐지되었다", + "UPLOAD_STRATEGY_CHOICE": "그것들을 업로드 하겠습니까", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "싱글 앨범", + "OR": "또는", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "앨범들 분리", + "SESSION_EXPIRED_MESSAGE": "세션이 만료되었습니다. 계속하려면 다시 로그인하세요", + "SESSION_EXPIRED": "세션 만료", + "PASSWORD_GENERATION_FAILED": "당신의 브라우저는 Ente의 암호화 표준을 충족하는 강력한 키를 생성할 수 없습니다. 모바일 앱이나 다른 브라우저를 사용하세요.", + "CHANGE_PASSWORD": "암호 변경하기", + "GO_BACK": "뒤로 가기", + "RECOVERY_KEY": "키 복구하기", + "SAVE_LATER": "나중에 저장하기", + "SAVE": "키 저장하기", + "RECOVERY_KEY_DESCRIPTION": "암호 분실시, 오직 이 키를 이용해야만 데이터를 복구할 수 있습니다.", "RECOVER_KEY_GENERATION_FAILED": "", "KEY_NOT_STORED_DISCLAIMER": "", "FORGOT_PASSWORD": "", @@ -126,15 +126,15 @@ "CONTACT_SUPPORT": "", "REQUEST_FEATURE": "", "SUPPORT": "", - "CONFIRM": "", - "CANCEL": "", - "LOGOUT": "", - "DELETE_ACCOUNT": "", - "DELETE_ACCOUNT_MESSAGE": "", - "LOGOUT_MESSAGE": "", - "CHANGE_EMAIL": "", - "OK": "", - "SUCCESS": "", + "CONFIRM": "확인", + "CANCEL": "취소", + "LOGOUT": "로그아웃", + "DELETE_ACCOUNT": "계정 삭제", + "DELETE_ACCOUNT_MESSAGE": "

회원가입에 사용한 이메일을 통해 {{emailID}} (으)로 메일을 보내주세요.

귀하의 요청은 72시간 내로 처리됩니다.

", + "LOGOUT_MESSAGE": "정말로 로그아웃 하시겠습니까?", + "CHANGE_EMAIL": "이메일 주소 변경", + "OK": "확인", + "SUCCESS": "성공", "ERROR": "", "MESSAGE": "", "INSTALL_MOBILE_APP": "", @@ -144,7 +144,7 @@ "SUBSCRIPTION": "", "SUBSCRIBE": "", "MANAGEMENT_PORTAL": "", - "MANAGE_FAMILY_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "가족 관리", "LEAVE_FAMILY_PLAN": "", "LEAVE": "", "LEAVE_FAMILY_CONFIRM": "", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/nl-NL/translation.json b/web/apps/photos/public/locales/nl-NL/translation.json index 2b2aa1111..6e9077e8f 100644 --- a/web/apps/photos/public/locales/nl-NL/translation.json +++ b/web/apps/photos/public/locales/nl-NL/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Inloggen", "SIGN_UP": "Registreren", - "NEW_USER": "Nieuw bij ente", + "NEW_USER": "Nieuw bij Ente", "EXISTING_USER": "Bestaande gebruiker", "ENTER_NAME": "Naam invoeren", "PUBLIC_UPLOADER_NAME_MESSAGE": "Voeg een naam toe zodat je vrienden weten wie ze moeten bedanken voor deze geweldige foto's!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Bestanden verwijderen?", "TRASH_FILE_TITLE": "Verwijder bestand?", "DELETE_FILES_TITLE": "Onmiddellijk verwijderen?", - "DELETE_FILES_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van je ente account.", + "DELETE_FILES_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van je Ente account.", "DELETE": "Verwijderen", "DELETE_OPTION": "Verwijderen (DEL)", "FAVORITE_OPTION": "Favoriet (L)", @@ -278,11 +278,11 @@ "LAST_EXPORT_TIME": "Tijd laatste export", "EXPORT_AGAIN": "Opnieuw synchroniseren", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokale opslag niet toegankelijk", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Je browser of een extensie blokkeert ente om gegevens op te slaan in de lokale opslag. Probeer deze pagina te laden na het aanpassen van de browser surfmodus.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Je browser of een extensie blokkeert Ente om gegevens op te slaan in de lokale opslag. Probeer deze pagina te laden na het aanpassen van de browser surfmodus.", "SEND_OTT": "Stuur OTP", "EMAIl_ALREADY_OWNED": "E-mail al in gebruik", - "ETAGS_BLOCKED": "

We kunnen de volgende bestanden niet uploaden vanwege uw browserconfiguratie.

Schakel alle extensies uit die mogelijk voorkomen dat ente eTags kan gebruiken om grote bestanden te uploaden, of gebruik onze desktop app voor een betrouwbaardere import ervaring.

", - "SKIPPED_VIDEOS_INFO": "

We ondersteunen het toevoegen van video's via openbare links momenteel niet.

Om video's te delen, meld je aan bij ente en deel met de beoogde ontvangers via hun e-mail

", + "ETAGS_BLOCKED": "

We kunnen de volgende bestanden niet uploaden vanwege uw browserconfiguratie.

Schakel alle extensies uit die mogelijk voorkomen dat Ente eTags kan gebruiken om grote bestanden te uploaden, of gebruik onze desktop app voor een betrouwbaardere import ervaring.

", + "SKIPPED_VIDEOS_INFO": "

We ondersteunen het toevoegen van video's via openbare links momenteel niet.

Om video's te delen, meld je aan bij Ente en deel met de beoogde ontvangers via hun e-mail

", "LIVE_PHOTOS_DETECTED": "De foto en video bestanden van je Live Photos zijn samengevoegd tot één enkel bestand", "RETRY_FAILED": "Probeer mislukte uploads nogmaals", "FAILED_UPLOADS": "Mislukte uploads ", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Niet-ondersteunde bestanden", "SUCCESSFUL_UPLOADS": "Succesvolle uploads", "SKIPPED_INFO": "Deze zijn overgeslagen omdat er bestanden zijn met overeenkomende namen in hetzelfde album", - "UNSUPPORTED_INFO": "ente ondersteunt deze bestandsformaten nog niet", + "UNSUPPORTED_INFO": "Ente ondersteunt deze bestandsformaten nog niet", "BLOCKED_UPLOADS": "Geblokkeerde uploads", "SKIPPED_VIDEOS": "Overgeslagen video's", "INPROGRESS_METADATA_EXTRACTION": "In behandeling", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Terugzetten naar album", "EMPTY_TRASH": "Prullenbak leegmaken", "EMPTY_TRASH_TITLE": "Prullenbak leegmaken?", - "EMPTY_TRASH_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van uw ente account.", + "EMPTY_TRASH_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van uw Ente account.", "LEAVE_SHARED_ALBUM": "Ja, verwijderen", "LEAVE_ALBUM": "Album verlaten", "LEAVE_SHARED_ALBUM_TITLE": "Gedeeld album verwijderen?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails gecomprimeerd", "FIX_THUMBNAIL": "Comprimeren", "FIX_THUMBNAIL_LATER": "Later comprimeren", - "REPLACE_THUMBNAIL_NOT_STARTED": "Sommige van uw video thumbnails kunnen worden gecomprimeerd om ruimte te besparen. Wilt u dat ente ze comprimeert?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Sommige van uw video thumbnails kunnen worden gecomprimeerd om ruimte te besparen. Wilt u dat Ente ze comprimeert?", "REPLACE_THUMBNAIL_COMPLETED": "Alle thumbnails zijn gecomprimeerd", "REPLACE_THUMBNAIL_NOOP": "Je hebt geen thumbnails die verder gecomprimeerd kunnen worden", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Kon sommige van uw thumbnails niet comprimeren, probeer het opnieuw", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Verificatie apparaat", "NO_DUPLICATES_FOUND": "Je hebt geen dubbele bestanden die kunnen worden gewist", "CLUB_BY_CAPTURE_TIME": "Samenvoegen op tijd", - "FILES": "Bestanden", - "EACH": "Elke", + "FILES": "bestanden", + "EACH": "elke", "DEDUPLICATE_BASED_ON_SIZE": "De volgende bestanden zijn samengevoegd op basis van hun groottes. Controleer en verwijder items waarvan je denkt dat ze dubbel zijn", "STOP_ALL_UPLOADS_MESSAGE": "Weet u zeker dat u wilt stoppen met alle uploads die worden uitgevoerd?", "STOP_UPLOADS_HEADER": "Stoppen met uploaden?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Gemonitorde mappen", "NO_FOLDERS_ADDED": "Nog geen mappen toegevoegd!", "FOLDERS_AUTOMATICALLY_MONITORED": "De mappen die u hier toevoegt worden automatisch gemonitord", - "UPLOAD_NEW_FILES_TO_ENTE": "Nieuwe bestanden uploaden naar ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Verwijderde bestanden van ente opruimen", + "UPLOAD_NEW_FILES_TO_ENTE": "Nieuwe bestanden uploaden naar Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Verwijderde bestanden van Ente opruimen", "ADD_FOLDER": "Map toevoegen", "STOP_WATCHING": "Stop monitoren", "STOP_WATCHING_FOLDER": "Stop monitoren van map?", - "STOP_WATCHING_DIALOG_MESSAGE": "Uw bestaande bestanden zullen niet worden verwijderd, maar ente stopt met het automatisch bijwerken van het gekoppelde ente album bij wijzigingen in deze map.", + "STOP_WATCHING_DIALOG_MESSAGE": "Uw bestaande bestanden zullen niet worden verwijderd, maar Ente stopt met het automatisch bijwerken van het gekoppelde Ente album bij wijzigingen in deze map.", "YES_STOP": "Ja, stop", "MONTH_SHORT": "mo", "YEAR": "jaar", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account", "FREE_PLAN_DESCRIPTION": "1 GB voor 1 jaar", "CURRENT_USAGE": "Huidig gebruik is {{usage}}", - "WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de ente mobiel/desktop app.", - "DRAG_AND_DROP_HINT": "Of sleep en plaats in het ente venster", + "WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de Ente mobiel/desktop app.", + "DRAG_AND_DROP_HINT": "Of sleep en plaats in het Ente venster", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Uw geüploade gegevens worden gepland voor verwijdering, en uw account zal permanent worden verwijderd.

Deze actie is onomkeerbaar.", "AUTHENTICATE": "Verifiëren", "UPLOADED_TO_SINGLE_COLLECTION": "Geüpload naar enkele collectie", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Geüpload naar verschillende collecties", "NEVERMIND": "Laat maar", "UPDATE_AVAILABLE": "Update beschikbaar", - "UPDATE_INSTALLABLE_MESSAGE": "Er staat een nieuwe versie van ente klaar om te worden geïnstalleerd.", + "UPDATE_INSTALLABLE_MESSAGE": "Er staat een nieuwe versie van Ente klaar om te worden geïnstalleerd.", "INSTALL_NOW": "Nu installeren", "INSTALL_ON_NEXT_LAUNCH": "Installeren bij volgende start", - "UPDATE_AVAILABLE_MESSAGE": "Er is een nieuwe versie van ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", + "UPDATE_AVAILABLE_MESSAGE": "Er is een nieuwe versie van Ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", "DOWNLOAD_AND_INSTALL": "Downloaden en installeren", "IGNORE_THIS_VERSION": "Negeer deze versie", "TODAY": "Vandaag", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "Meer details", "ENABLE_FACE_SEARCH": "Zoeken op gezichten inschakelen", "ENABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten inschakelen?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

Als u zoeken op gezichten inschakelt, analyseert ente de gezichtsgeometrie uit uw foto's. Dit gebeurt op uw apparaat en alle gegenereerde biometrische gegevens worden end-to-end versleuteld.

Klik hier voor meer informatie over deze functie in ons privacybeleid

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

Als u zoeken op gezichten inschakelt, analyseert Ente de gezichtsgeometrie uit uw foto's. Dit gebeurt op uw apparaat en alle gegenereerde biometrische gegevens worden end-to-end versleuteld.

Klik hier voor meer informatie over deze functie in ons privacybeleid

", "DISABLE_BETA": "Bèta uitschakelen", "DISABLE_FACE_SEARCH": "Zoeken op gezichten uitschakelen", "DISABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten uitschakelen?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente zal stoppen met het analyseren van de gezichtsgeometrie, en zal ML zoeken (beta) uitschakelen

U kan zoeken op gezichten opnieuw inschakelen wanneer u wilt, dus deze handeling is veilig.

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente zal stoppen met het analyseren van de gezichtsgeometrie, en zal ML zoeken (beta) uitschakelen

U kan zoeken op gezichten opnieuw inschakelen wanneer u wilt, dus deze handeling is veilig.

", "ADVANCED": "Geavanceerd", - "FACE_SEARCH_CONFIRMATION": "Ik begrijp het, en wil ente toestaan om gezichten te analyseren", + "FACE_SEARCH_CONFIRMATION": "Ik begrijp het, en wil Ente toestaan om gezichten te analyseren", "LABS": "Lab's", "YOURS": "jouw", "PASSPHRASE_STRENGTH_WEAK": "Wachtwoord sterkte: Zwak", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Live foto", "CONVERT": "Converteren", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Weet u zeker dat u de editor wilt afsluiten?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download uw bewerkte afbeelding of sla een kopie op in ente om uw wijzigingen te behouden.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download uw bewerkte afbeelding of sla een kopie op in Ente om uw wijzigingen te behouden.", "BRIGHTNESS": "Helderheid", "CONTRAST": "Contrast", "SATURATION": "Saturatie", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Verticaal spiegelen", "FLIP_HORIZONTALLY": "Horizontaal spiegelen", "DOWNLOAD_EDITED": "Download Bewerkt", - "SAVE_A_COPY_TO_ENTE": "Kopie in ente opslaan", + "SAVE_A_COPY_TO_ENTE": "Kopie in Ente opslaan", "RESTORE_ORIGINAL": "Origineel herstellen", "TRANSFORM": "Transformeer", "COLORS": "Kleuren", @@ -622,20 +622,33 @@ "FASTER_UPLOAD_DESCRIPTION": "Uploaden door nabije servers", "MAGIC_SEARCH_STATUS": "Magische Zoekfunctie Status", "INDEXED_ITEMS": "Geïndexeerde bestanden", - "CAST_ALBUM_TO_TV": "", - "ENTER_CAST_PIN_CODE": "", - "PAIR_DEVICE_TO_TV": "", - "TV_NOT_FOUND": "", - "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", - "PAIR_WITH_PIN": "", - "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", - "VISIT_CAST_ENTE_IO": "", - "CAST_AUTO_PAIR_FAILED": "", + "CAST_ALBUM_TO_TV": "Album afspelen op TV", + "ENTER_CAST_PIN_CODE": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.", + "PAIR_DEVICE_TO_TV": "Koppel apparaten", + "TV_NOT_FOUND": "TV niet gevonden. Heeft u de pincode correct ingevoerd?", + "AUTO_CAST_PAIR": "Automatisch koppelen", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Automatisch koppelen vereist verbinding met Google-servers en werkt alleen met apparaten die door Chromecast worden ondersteund. Google zal geen gevoelige gegevens ontvangen, zoals uw foto's.", + "PAIR_WITH_PIN": "Koppelen met PIN", + "CHOOSE_DEVICE_FROM_BROWSER": "Kies een compatibel apparaat uit de browser popup.", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Koppelen met PIN werkt op elk groot schermapparaat waarop u uw album wilt afspelen.", + "VISIT_CAST_ENTE_IO": "Bezoek {{url}} op het apparaat dat je wilt koppelen.", + "CAST_AUTO_PAIR_FAILED": "Auto koppelen van Chromecast is mislukt. Probeer het opnieuw.", "CACHE_DIRECTORY": "Cache map", - "PASSKEYS": "", "FREEHAND": "Losse hand", "APPLY_CROP": "Bijsnijden toepassen", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Passkeys verwijderen", + "DELETE_PASSKEY_CONFIRMATION": "Weet je zeker dat je deze passkey wilt verwijderen? Deze actie is onomkeerbaar.", + "RENAME_PASSKEY": "Hernoem passkeys", + "ADD_PASSKEY": "Passkeys toevoegen", + "ENTER_PASSKEY_NAME": "Voer passkey naam in", + "PASSKEYS_DESCRIPTION": "Passkeys zijn een moderne en veilige tweede factor beveiliging voor je Ente account. Ze gebruiken biometrische authenticatie op je apparaat voor gemak en veiligheid.", + "CREATED_AT": "Aangemaakt op", + "PASSKEY_LOGIN_FAILED": "Passkey login mislukt", + "PASSKEY_LOGIN_URL_INVALID": "De inlog-URL is ongeldig.", + "PASSKEY_LOGIN_ERRORED": "Er is een fout opgetreden tijdens het inloggen met een passkey.", + "TRY_AGAIN": "Probeer opnieuw", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Volg de stappen van je browser om door te gaan met inloggen.", + "LOGIN_WITH_PASSKEY": "Inloggen met passkey" } diff --git a/web/apps/photos/public/locales/pt-BR/translation.json b/web/apps/photos/public/locales/pt-BR/translation.json index 4eaa398c4..10a21ae90 100644 --- a/web/apps/photos/public/locales/pt-BR/translation.json +++ b/web/apps/photos/public/locales/pt-BR/translation.json @@ -7,8 +7,8 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Entrar", "SIGN_UP": "Registrar", - "NEW_USER": "Novo no ente", - "EXISTING_USER": "Utilizador existente", + "NEW_USER": "Novo no Ente", + "EXISTING_USER": "Usuário existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", "ENTER_EMAIL": "Insira o endereço de e-mail", @@ -31,7 +31,7 @@ "VERIFY_PASSPHRASE": "Iniciar sessão", "INCORRECT_PASSPHRASE": "Palavra-passe incorreta", "ENTER_ENC_PASSPHRASE": "Por favor, digite uma senha que podemos usar para criptografar seus dados", - "PASSPHRASE_DISCLAIMER": "Não armazenamos sua senha, portanto, se você esquecê-la, não poderemos ajudarna recuperação de seus dados sem uma chave de recuperação.", + "PASSPHRASE_DISCLAIMER": "Não armazenamos sua senha, portanto, se você esquecê-la, não poderemos ajudar na recuperação de seus dados sem uma chave de recuperação.", "WELCOME_TO_ENTE_HEADING": "Bem-vindo ao ", "WELCOME_TO_ENTE_SUBHEADING": "Armazenamento criptografado de ponta a ponta de fotos e compartilhamento", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Onde suas melhores fotos vivem", @@ -124,7 +124,7 @@ "NO_RECOVERY_KEY_MESSAGE": "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", "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Por favor, envie um e-mail para {{emailID}} a partir do seu endereço de e-mail registrado", "CONTACT_SUPPORT": "Falar com o suporte", - "REQUEST_FEATURE": "Solicitar Funcionalidade", + "REQUEST_FEATURE": "Solicitar recurso", "SUPPORT": "Suporte", "CONFIRM": "Confirmar", "CANCEL": "Cancelar", @@ -227,12 +227,12 @@ "INDEXING_SCHEDULED": "Indexação está programada...", "ANALYZING_PHOTOS": "Indexando fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", "INDEXING_PEOPLE": "Indexando pessoas em {{indexStatus.nSyncedFiles,number}} fotos...", - "INDEXING_DONE": "", + "INDEXING_DONE": "Foram indexadas {{indexStatus.nSyncedFiles,number}} fotos", "UNIDENTIFIED_FACES": "rostos não identificados", "OBJECTS": "objetos", "TEXT": "texto", - "INFO": "Informação ", - "INFO_OPTION": "Informação (I)", + "INFO": "Informações ", + "INFO_OPTION": "Informações (I)", "FILE_NAME": "Nome do arquivo", "CAPTION_PLACEHOLDER": "Adicionar uma descrição", "LOCATION": "Local", @@ -306,7 +306,7 @@ "ARCHIVE": "Arquivar", "FAVORITES": "Favoritos", "ARCHIVE_COLLECTION": "Arquivar álbum", - "ARCHIVE_SECTION_NAME": "Arquivar", + "ARCHIVE_SECTION_NAME": "Arquivado", "ALL_SECTION_NAME": "Todos", "MOVE_TO_COLLECTION": "Mover para álbum", "UNARCHIVE": "Desarquivar", @@ -347,15 +347,15 @@ "REPLACE_THUMBNAIL_NOOP": "Você não tem nenhuma miniatura que possa ser compactadas mais", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Não foi possível compactar algumas das suas miniaturas, por favor tente novamente", "FIX_CREATION_TIME": "Corrigir hora", - "FIX_CREATION_TIME_IN_PROGRESS": "", - "CREATION_TIME_UPDATED": "", + "FIX_CREATION_TIME_IN_PROGRESS": "Corrigindo horário", + "CREATION_TIME_UPDATED": "Hora do arquivo atualizado", "UPDATE_CREATION_TIME_NOT_STARTED": "Selecione a carteira que você deseja usar", "UPDATE_CREATION_TIME_COMPLETED": "Todos os arquivos atualizados com sucesso", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "A atualização do horário falhou para alguns arquivos, por favor, tente novamente", "CAPTION_CHARACTER_LIMIT": "5000 caracteres no máximo", - "DATE_TIME_ORIGINAL": "", - "DATE_TIME_DIGITIZED": "", - "METADATA_DATE": "", + "DATE_TIME_ORIGINAL": "Data e Hora Original", + "DATE_TIME_DIGITIZED": "Data e Hora Digitalizada", + "METADATA_DATE": "Data de Metadados", "CUSTOM_TIME": "Tempo personalizado", "REOPEN_PLAN_SELECTOR_MODAL": "Reabrir planos", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Falha ao abrir planos", @@ -408,7 +408,7 @@ "SHARED_USING": "Compartilhar usando ", "ENTE_IO": "ente.io", "SHARING_REFERRAL_CODE": "Use o código {{referralCode}} para obter 10 GB de graça", - "LIVE": "", + "LIVE": "AO VIVO", "DISABLE_PASSWORD": "Desativar bloqueio por senha", "DISABLE_PASSWORD_MESSAGE": "Tem certeza que deseja desativar o bloqueio por senha?", "PASSWORD_LOCK": "Bloqueio de senha", @@ -417,12 +417,12 @@ "UPLOAD_FILES": "Arquivo", "UPLOAD_DIRS": "Pasta", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", - "DEDUPLICATE_FILES": "Arquivos Deduplicados", + "DEDUPLICATE_FILES": "Arquivos duplicados", "AUTHENTICATOR_SECTION": "Autenticação", "NO_DUPLICATES_FOUND": "Você não tem arquivos duplicados que possam ser limpos", "CLUB_BY_CAPTURE_TIME": "Agrupar por tempo de captura", - "FILES": "Arquivos", - "EACH": "Cada", + "FILES": "arquivos", + "EACH": "cada", "DEDUPLICATE_BASED_ON_SIZE": "Os seguintes arquivos foram listados com base em seus tamanhos, por favor, reveja e exclua os itens que você acredita que são duplicados", "STOP_ALL_UPLOADS_MESSAGE": "Tem certeza que deseja parar todos os envios em andamento?", "STOP_UPLOADS_HEADER": "Parar envios?", @@ -506,8 +506,8 @@ "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente irá parar de processar geometria facial.

Você pode reativar o reconhecimento facial novamente, se desejar, então esta operação está segura.

", "ADVANCED": "Avançado", "FACE_SEARCH_CONFIRMATION": "Eu entendo, e desejo permitir que o ente processe a geometria do rosto", - "LABS": "", - "YOURS": "", + "LABS": "Laboratórios", + "YOURS": "seu", "PASSPHRASE_STRENGTH_WEAK": "Força da senha: fraca", "PASSPHRASE_STRENGTH_MODERATE": "Força da senha: moderada", "PASSPHRASE_STRENGTH_STRONG": "Força da senha: forte", @@ -570,10 +570,10 @@ "FEEDBACK_REQUIRED": "Por favor, ajude-nos com esta informação", "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "O que o outro serviço faz melhor?", "RECOVER_TWO_FACTOR": "Recuperar dois fatores", - "at": "", + "at": "em", "AUTH_NEXT": "próximo", "AUTH_DOWNLOAD_MOBILE_APP": "Baixe nosso aplicativo móvel para gerenciar seus segredos", - "HIDDEN": "Escondido", + "HIDDEN": "Oculto", "HIDE": "Ocultar", "UNHIDE": "Desocultar", "UNHIDE_TO_COLLECTION": "Reexibir para o álbum", @@ -604,12 +604,12 @@ "BLUR": "Desfoque", "INVERT_COLORS": "Inverter Cores", "ASPECT_RATIO": "Proporção da imagem", - "SQUARE": "", + "SQUARE": "Quadrado", "ROTATE_LEFT": "Girar para a Esquerda", "ROTATE_RIGHT": "Girar para a Direita", "FLIP_VERTICALLY": "Inverter verticalmente", "FLIP_HORIZONTALLY": "Inverter horizontalmente", - "DOWNLOAD_EDITED": "Transferência Editada", + "DOWNLOAD_EDITED": "Baixar Editado", "SAVE_A_COPY_TO_ENTE": "Salvar uma cópia para o ente", "RESTORE_ORIGINAL": "Restaurar original", "TRANSFORM": "Transformar", @@ -631,11 +631,24 @@ "PAIR_WITH_PIN": "Parear com PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Escolha um dispositivo compatível com casts no navegador popup.", "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Parear com o PIN funciona para qualquer dispositivo de tela grande onde você deseja reproduzir seu álbum.", - "VISIT_CAST_ENTE_IO": "Acesse cast.ente.io no dispositivo que você deseja parear.", + "VISIT_CAST_ENTE_IO": "Acesse
{{url}} no dispositivo que você deseja parear.", "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.", "CACHE_DIRECTORY": "Pasta de Cache", - "PASSKEYS": "", - "FREEHAND": "", + "FREEHAND": "Mão livre", "APPLY_CROP": "Aplicar Recorte", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar." + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", + "PASSKEYS": "Chaves de acesso", + "DELETE_PASSKEY": "Excluir chave de acesso", + "DELETE_PASSKEY_CONFIRMATION": "Tem certeza de que deseja excluir esta chave de acesso? Esta ação é irreversível.", + "RENAME_PASSKEY": "Renomear chave de acesso", + "ADD_PASSKEY": "Adicionar chave de acesso", + "ENTER_PASSKEY_NAME": "Inserir nome da chave de acesso", + "PASSKEYS_DESCRIPTION": "As chaves de acesso são um moderno e seguro segundo fator de sua conta Ente. Elas usam a autenticação biométrica no dispositivo para fins de conveniência e segurança.", + "CREATED_AT": "Criado em", + "PASSKEY_LOGIN_FAILED": "Falha ao iniciar sessão com a chave de acesso", + "PASSKEY_LOGIN_URL_INVALID": "URL de login inválida.", + "PASSKEY_LOGIN_ERRORED": "Ocorreu um erro ao entrar com a chave de acesso.", + "TRY_AGAIN": "Tente novamente", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Siga os passos do seu navegador para continuar acessando.", + "LOGIN_WITH_PASSKEY": "Entrar com a chave de acesso" } diff --git a/web/apps/photos/public/locales/pt-PT/translation.json b/web/apps/photos/public/locales/pt-PT/translation.json index ac7d17968..cf06665fe 100644 --- a/web/apps/photos/public/locales/pt-PT/translation.json +++ b/web/apps/photos/public/locales/pt-PT/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Entrar", "SIGN_UP": "Registar", - "NEW_USER": "Novo no ente", + "NEW_USER": "Novo no Ente", "EXISTING_USER": "Utilizador existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/ru-RU/translation.json b/web/apps/photos/public/locales/ru-RU/translation.json index 0e9715563..63a50de40 100644 --- a/web/apps/photos/public/locales/ru-RU/translation.json +++ b/web/apps/photos/public/locales/ru-RU/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Веб, ПК", "LOGIN": "Авторизоваться", "SIGN_UP": "Регистрация", - "NEW_USER": "Новенький в ente", + "NEW_USER": "Новенький в Ente", "EXISTING_USER": "Существующий пользователь", "ENTER_NAME": "Введите имя", "PUBLIC_UPLOADER_NAME_MESSAGE": "Добавьте имя, чтобы ваши друзья знали, кого благодарить за эти замечательные фотографии!", @@ -31,9 +31,9 @@ "VERIFY_PASSPHRASE": "Войти", "INCORRECT_PASSPHRASE": "Неверный пароль", "ENTER_ENC_PASSPHRASE": "Пожалуйста, введите пароль, который мы можем использовать для шифрования ваших данных", - "PASSPHRASE_DISCLAIMER": "", + "PASSPHRASE_DISCLAIMER": "Мы не храним ваш пароль, поэтому, если вы его забудете,\nмы ничем не сможем вам помочь\nвосстановите ваши данные без ключа восстановления.", "WELCOME_TO_ENTE_HEADING": "Добро пожаловать в ", - "WELCOME_TO_ENTE_SUBHEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "Сквозное зашифрованное хранение фотографий и общий доступ к ним", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Где живут ваши лучшие фотографии", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Генерируем ключи шифрования...", "PASSPHRASE_HINT": "Пароль", @@ -85,7 +85,7 @@ "NEXT": "Следующий (→)", "TITLE_PHOTOS": "Ente Фото", "TITLE_ALBUMS": "Ente Фото", - "TITLE_AUTH": "", + "TITLE_AUTH": "Ente Auth", "UPLOAD_FIRST_PHOTO": "Загрузите своё первое фото", "IMPORT_YOUR_FOLDERS": "Импортируйте папки", "UPLOAD_DROPZONE_MESSAGE": "Перетащите для резервного копирования файлов", @@ -97,7 +97,7 @@ "DELETE": "Удалить", "DELETE_OPTION": "Удалить (DEL)", "FAVORITE_OPTION": "Избранное (L)", - "UNFAVORITE_OPTION": "", + "UNFAVORITE_OPTION": "Неблагоприятный фактор (L)", "MULTI_FOLDER_UPLOAD": "Обнаружено несколько папок", "UPLOAD_STRATEGY_CHOICE": "Вы хотите загрузить их в", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Один альбом", @@ -138,7 +138,7 @@ "ERROR": "Ошибка", "MESSAGE": "Сообщение", "INSTALL_MOBILE_APP": "Установите наше приложение Android или iOS для автоматического резервного копирования всех ваших фотографий", - "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP_MESSAGE": "К сожалению, в настоящее время эта операция поддерживается только в нашем настольном приложении", "DOWNLOAD_APP": "Загрузить приложение для компьютера", "EXPORT": "Экспортировать данные", "SUBSCRIPTION": "Подписка", @@ -155,16 +155,16 @@ "FREE_SUBSCRIPTION_INFO": "Вы используете бесплатный тарифный план, истекающий {{date, dateTime}}", "FAMILY_SUBSCRIPTION_INFO": "Вы используете семейный план, управляемый", "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Продление {{date, dateTime}}", - "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", - "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", - "ADD_ON_AVAILABLE_TILL": "", - "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", - "SUBSCRIPTION_PURCHASE_SUCCESS": "", - "SUBSCRIPTION_PURCHASE_CANCELLED": "", - "SUBSCRIPTION_PURCHASE_FAILED": "", - "SUBSCRIPTION_UPDATE_FAILED": "", - "UPDATE_PAYMENT_METHOD_MESSAGE": "", - "STRIPE_AUTHENTICATION_FAILED": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Заканчивается на {{date, dateTime}}", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Ваша подписка будет отменена в {{date, dateTime}}", + "ADD_ON_AVAILABLE_TILL": "Ваш {{storage, string}} дополнение действует до {{date, dateTime}}", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Вы превысили свою квоту на хранение, пожалуйста upgrade", + "SUBSCRIPTION_PURCHASE_SUCCESS": "

Мы получили ваш платеж

Ваша подписка действительна до {{date, dateTime}}

", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Ваша покупка была отменена, пожалуйста, повторите попытку, если вы хотите подписаться", + "SUBSCRIPTION_PURCHASE_FAILED": "Не удалось приобрести подписку, пожалуйста, повторите попытку", + "SUBSCRIPTION_UPDATE_FAILED": "Не удалось обновить подписку, пожалуйста, повторите попытку", + "UPDATE_PAYMENT_METHOD_MESSAGE": "Приносим извинения, при попытке списать средства с вашей карты произошел сбой платежа. Пожалуйста, измените способ оплаты и повторите попытку", + "STRIPE_AUTHENTICATION_FAILED": "Мы не можем подтвердить подлинность вашего способа оплаты. пожалуйста, выберите другой способ оплаты и повторите попытку", "UPDATE_PAYMENT_METHOD": "Обновить платёжную информацию", "MONTHLY": "Ежемесячно", "YEARLY": "Ежегодно", @@ -214,12 +214,12 @@ "THING": "Содержимое", "FILE_CAPTION": "Описание", "FILE_TYPE": "Тип файла", - "CLIP": "" + "CLIP": "Магия" }, "photos_count_zero": "Воспоминания отсутствуют", - "photos_count_one": "", - "photos_count_other": "", - "TERMS_AND_CONDITIONS": "", + "photos_count_one": "1 память", + "photos_count_other": "{{count, number}} воспоминания", + "TERMS_AND_CONDITIONS": "Я согласен с тем, что условия и политика конфиденциальности", "ADD_TO_COLLECTION": "Добавить в альбом", "SELECTED": "выбрано", "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Это видео нельзя воспроизвести в вашем браузере", @@ -229,28 +229,28 @@ "INDEXING_PEOPLE": "Индексирование людей на {{indexStatus.nSyncedFiles,number}} фотографиях...", "INDEXING_DONE": "Проиндексировано {{indexStatus.nSyncedFiles,number}} фотографий", "UNIDENTIFIED_FACES": "нераспознанные лица", - "OBJECTS": "", - "TEXT": "", - "INFO": "", - "INFO_OPTION": "", - "FILE_NAME": "", - "CAPTION_PLACEHOLDER": "", - "LOCATION": "", - "SHOW_ON_MAP": "", - "MAP": "", - "MAP_SETTINGS": "", - "ENABLE_MAPS": "", - "ENABLE_MAP": "", - "DISABLE_MAPS": "", - "ENABLE_MAP_DESCRIPTION": "", - "DISABLE_MAP_DESCRIPTION": "", - "DISABLE_MAP": "", - "DETAILS": "", - "VIEW_EXIF": "", - "NO_EXIF": "", - "EXIF": "", - "ISO": "", - "TWO_FACTOR": "", + "OBJECTS": "объекты", + "TEXT": "текст", + "INFO": "Информация ", + "INFO_OPTION": "Информация (I)", + "FILE_NAME": "Имя файла", + "CAPTION_PLACEHOLDER": "Добавьте описание", + "LOCATION": "Местоположение", + "SHOW_ON_MAP": "Просмотр на OpenStreetMap", + "MAP": "Карта", + "MAP_SETTINGS": "Настройки карты", + "ENABLE_MAPS": "Включить карты?", + "ENABLE_MAP": "Включить отображение", + "DISABLE_MAPS": "Отключить карты?", + "ENABLE_MAP_DESCRIPTION": "

Это отобразит ваши фотографии на карте мира.

Карта размещена на OpenStreetMap, и точные местоположения ваших фотографий никогда не публикуются.

Вы можете отключить эту функцию в любое время в настройках.

", + "DISABLE_MAP_DESCRIPTION": "

Это приведет к отключению отображения ваших фотографий на карте мира.

Вы можете включить эту функцию в любое время в настройках.

", + "DISABLE_MAP": "Отключить карту", + "DETAILS": "Подробности", + "VIEW_EXIF": "Просмотр всех данных EXIF", + "NO_EXIF": "Нет данных EXIF", + "EXIF": "EXIF", + "ISO": "ISO", + "TWO_FACTOR": "Двухфакторный", "TWO_FACTOR_AUTHENTICATION": "Двухфакторная аутентификация", "TWO_FACTOR_QR_INSTRUCTION": "Сканируйте QR-код ниже с вашим любимым приложением для проверки подлинности", "ENTER_CODE_MANUALLY": "Введите код вручную", @@ -260,17 +260,17 @@ "ENABLE": "Включить", "LOST_DEVICE": "Потеряно двухфакторное устройство", "INCORRECT_CODE": "Неверный код", - "TWO_FACTOR_INFO": "", + "TWO_FACTOR_INFO": "Добавьте дополнительный уровень безопасности, запросив для входа в свою учетную запись не только адрес электронной почты и пароль", "DISABLE_TWO_FACTOR_LABEL": "Отключить двухфакторную аутентификацию", - "UPDATE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "Обновите свое устройство аутентификации", "DISABLE": "Отключить", "RECONFIGURE": "Перенастроить", "UPDATE_TWO_FACTOR": "Обновить двухфакторную аутентификацию", - "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE_TWO_FACTOR_MESSAGE": "Дальнейшая переадресация приведет к аннулированию всех ранее настроенных средств проверки подлинности", "UPDATE": "Обновить", "DISABLE_TWO_FACTOR": "Отключить двухфакторную аутентификацию", "DISABLE_TWO_FACTOR_MESSAGE": "Вы уверены, что хотите отключить двухфакторную аутентификацию", - "TWO_FACTOR_DISABLE_FAILED": "", + "TWO_FACTOR_DISABLE_FAILED": "Не удалось отключить два фактора, пожалуйста, повторите попытку", "EXPORT_DATA": "Экспортировать данные", "SELECT_FOLDER": "Выбрать папку", "DESTINATION": "Место назначения", @@ -278,309 +278,309 @@ "LAST_EXPORT_TIME": "Время последнего экспорта", "EXPORT_AGAIN": "Синхронизировать заново", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Локальное хранилище недоступно", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ваш браузер или какое-либо дополнение блокирует сохранение данных в локальном хранилище. пожалуйста, попробуйте загрузить эту страницу после переключения режима просмотра.", "SEND_OTT": "Отправить одноразовый код", "EMAIl_ALREADY_OWNED": "Почта уже использована", - "ETAGS_BLOCKED": "", - "SKIPPED_VIDEOS_INFO": "", - "LIVE_PHOTOS_DETECTED": "", - "RETRY_FAILED": "", - "FAILED_UPLOADS": "", - "SKIPPED_FILES": "", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", - "UNSUPPORTED_FILES": "", - "SUCCESSFUL_UPLOADS": "", - "SKIPPED_INFO": "", - "UNSUPPORTED_INFO": "", - "BLOCKED_UPLOADS": "", - "SKIPPED_VIDEOS": "", - "INPROGRESS_METADATA_EXTRACTION": "", - "INPROGRESS_UPLOADS": "", - "TOO_LARGE_UPLOADS": "", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", - "TOO_LARGE_INFO": "", - "THUMBNAIL_GENERATION_FAILED_INFO": "", - "UPLOAD_TO_COLLECTION": "", - "UNCATEGORIZED": "", - "ARCHIVE": "", - "FAVORITES": "", - "ARCHIVE_COLLECTION": "", - "ARCHIVE_SECTION_NAME": "", - "ALL_SECTION_NAME": "", - "MOVE_TO_COLLECTION": "", - "UNARCHIVE": "", - "UNARCHIVE_COLLECTION": "", - "HIDE_COLLECTION": "", - "UNHIDE_COLLECTION": "", - "MOVE": "", - "ADD": "", - "REMOVE": "", - "YES_REMOVE": "", - "REMOVE_FROM_COLLECTION": "", - "TRASH": "", - "MOVE_TO_TRASH": "", - "TRASH_FILES_MESSAGE": "", - "TRASH_FILE_MESSAGE": "", - "DELETE_PERMANENTLY": "", - "RESTORE": "", - "RESTORE_TO_COLLECTION": "", - "EMPTY_TRASH": "", - "EMPTY_TRASH_TITLE": "", - "EMPTY_TRASH_MESSAGE": "", - "LEAVE_SHARED_ALBUM": "", - "LEAVE_ALBUM": "", - "LEAVE_SHARED_ALBUM_TITLE": "", - "LEAVE_SHARED_ALBUM_MESSAGE": "", - "NOT_FILE_OWNER": "", - "CONFIRM_SELF_REMOVE_MESSAGE": "", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", - "SORT_BY_CREATION_TIME_ASCENDING": "", - "SORT_BY_UPDATION_TIME_DESCENDING": "", - "SORT_BY_NAME": "", - "COMPRESS_THUMBNAILS": "", - "THUMBNAIL_REPLACED": "", - "FIX_THUMBNAIL": "", - "FIX_THUMBNAIL_LATER": "", - "REPLACE_THUMBNAIL_NOT_STARTED": "", - "REPLACE_THUMBNAIL_COMPLETED": "", - "REPLACE_THUMBNAIL_NOOP": "", - "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", - "FIX_CREATION_TIME": "", - "FIX_CREATION_TIME_IN_PROGRESS": "", - "CREATION_TIME_UPDATED": "", - "UPDATE_CREATION_TIME_NOT_STARTED": "", - "UPDATE_CREATION_TIME_COMPLETED": "", - "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", - "CAPTION_CHARACTER_LIMIT": "", - "DATE_TIME_ORIGINAL": "", - "DATE_TIME_DIGITIZED": "", - "METADATA_DATE": "", - "CUSTOM_TIME": "", - "REOPEN_PLAN_SELECTOR_MODAL": "", - "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", - "INSTALL": "", - "SHARING_DETAILS": "", - "MODIFY_SHARING": "", - "ADD_COLLABORATORS": "", - "ADD_NEW_EMAIL": "", - "shared_with_people_zero": "", - "shared_with_people_one": "", - "shared_with_people_other": "", - "participants_zero": "", - "participants_one": "", - "participants_other": "", - "ADD_VIEWERS": "", - "PARTICIPANTS": "", - "CHANGE_PERMISSIONS_TO_VIEWER": "", - "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", - "CONVERT_TO_VIEWER": "", - "CONVERT_TO_COLLABORATOR": "", - "CHANGE_PERMISSION": "", - "REMOVE_PARTICIPANT": "", - "CONFIRM_REMOVE": "", - "MANAGE": "", - "ADDED_AS": "", - "COLLABORATOR_RIGHTS": "", - "REMOVE_PARTICIPANT_HEAD": "", - "OWNER": "", - "COLLABORATORS": "", - "ADD_MORE": "", - "VIEWERS": "", - "OR_ADD_EXISTING": "", - "REMOVE_PARTICIPANT_MESSAGE": "", - "NOT_FOUND": "", - "LINK_EXPIRED": "", - "LINK_EXPIRED_MESSAGE": "", - "MANAGE_LINK": "", - "LINK_TOO_MANY_REQUESTS": "", - "FILE_DOWNLOAD": "", - "LINK_PASSWORD_LOCK": "", - "PUBLIC_COLLECT": "", - "LINK_DEVICE_LIMIT": "", - "NO_DEVICE_LIMIT": "", - "LINK_EXPIRY": "", - "NEVER": "", - "DISABLE_FILE_DOWNLOAD": "", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "", - "MALICIOUS_CONTENT": "", - "COPYRIGHT": "", - "SHARED_USING": "", - "ENTE_IO": "", - "SHARING_REFERRAL_CODE": "", - "LIVE": "", - "DISABLE_PASSWORD": "", - "DISABLE_PASSWORD_MESSAGE": "", - "PASSWORD_LOCK": "", - "LOCK": "", - "DOWNLOAD_UPLOAD_LOGS": "", - "UPLOAD_FILES": "", - "UPLOAD_DIRS": "", - "UPLOAD_GOOGLE_TAKEOUT": "", - "DEDUPLICATE_FILES": "", - "AUTHENTICATOR_SECTION": "", - "NO_DUPLICATES_FOUND": "", - "CLUB_BY_CAPTURE_TIME": "", - "FILES": "", - "EACH": "", - "DEDUPLICATE_BASED_ON_SIZE": "", - "STOP_ALL_UPLOADS_MESSAGE": "", - "STOP_UPLOADS_HEADER": "", - "YES_STOP_UPLOADS": "", - "STOP_DOWNLOADS_HEADER": "", - "YES_STOP_DOWNLOADS": "", - "STOP_ALL_DOWNLOADS_MESSAGE": "", - "albums_one": "", - "albums_other": "", - "ALL_ALBUMS": "", - "ALBUMS": "", - "ALL_HIDDEN_ALBUMS": "", - "HIDDEN_ALBUMS": "", - "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", - "ENTER_TWO_FACTOR_OTP": "", - "CREATE_ACCOUNT": "", - "COPIED": "", - "CANVAS_BLOCKED_TITLE": "", - "CANVAS_BLOCKED_MESSAGE": "", - "WATCH_FOLDERS": "", - "UPGRADE_NOW": "", - "RENEW_NOW": "", - "STORAGE": "", - "USED": "", - "YOU": "", - "FAMILY": "", - "FREE": "", - "OF": "", - "WATCHED_FOLDERS": "", - "NO_FOLDERS_ADDED": "", - "FOLDERS_AUTOMATICALLY_MONITORED": "", - "UPLOAD_NEW_FILES_TO_ENTE": "", - "REMOVE_DELETED_FILES_FROM_ENTE": "", - "ADD_FOLDER": "", - "STOP_WATCHING": "", - "STOP_WATCHING_FOLDER": "", - "STOP_WATCHING_DIALOG_MESSAGE": "", - "YES_STOP": "", - "MONTH_SHORT": "", - "YEAR": "", - "FAMILY_PLAN": "", - "DOWNLOAD_LOGS": "", - "DOWNLOAD_LOGS_MESSAGE": "", - "CHANGE_FOLDER": "", - "TWO_MONTHS_FREE": "", - "GB": "", - "POPULAR": "", - "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", - "CURRENT_USAGE": "", - "WEAK_DEVICE": "", - "DRAG_AND_DROP_HINT": "", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", - "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": "", - "NAME_PLACEHOLDER": "", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", - "CHOSE_THEME": "", - "ML_SEARCH": "", - "ENABLE_ML_SEARCH_DESCRIPTION": "", - "ML_MORE_DETAILS": "", - "ENABLE_FACE_SEARCH": "", - "ENABLE_FACE_SEARCH_TITLE": "", - "ENABLE_FACE_SEARCH_DESCRIPTION": "", - "DISABLE_BETA": "", - "DISABLE_FACE_SEARCH": "", - "DISABLE_FACE_SEARCH_TITLE": "", - "DISABLE_FACE_SEARCH_DESCRIPTION": "", - "ADVANCED": "", - "FACE_SEARCH_CONFIRMATION": "", - "LABS": "", - "YOURS": "", - "PASSPHRASE_STRENGTH_WEAK": "", - "PASSPHRASE_STRENGTH_MODERATE": "", - "PASSPHRASE_STRENGTH_STRONG": "", - "PREFERENCES": "", - "LANGUAGE": "", - "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", - "SUBSCRIPTION_VERIFICATION_ERROR": "", + "ETAGS_BLOCKED": "

Мы не смогли загрузить следующие файлы из-за настроек вашего браузера.

Пожалуйста, отключите все дополнения, которые могут помешать Ente использоватьэТажи для загрузки больших файлов или использования нашего настольное приложение для более надежного осуществления импорта./p>", + "SKIPPED_VIDEOS_INFO": "

В настоящее время мы не поддерживаем добавление видео по общедоступным ссылкам.

Чтобы поделиться видео, пожалуйста, зарегистрируйтесь на Ente и поделитесь с получателями, используя их электронную почту.

", + "LIVE_PHOTOS_DETECTED": "Фото- и видеофайлы из ваших Live Photos были объединены в один файл", + "RETRY_FAILED": "Повторите попытку неудачной загрузки", + "FAILED_UPLOADS": "Неудачная загрузка ", + "SKIPPED_FILES": "Игнорируемые загрузки", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "Не удалось создать миниатюру", + "UNSUPPORTED_FILES": "Неподдерживаемые файлы", + "SUCCESSFUL_UPLOADS": "Успешные загрузки", + "SKIPPED_INFO": "Пропустил их, так как в одном альбоме есть файлы с одинаковыми названиями", + "UNSUPPORTED_INFO": "Ente пока не поддерживает эти форматы файлов", + "BLOCKED_UPLOADS": "Заблокированные загрузки", + "SKIPPED_VIDEOS": "Пропущенные видео", + "INPROGRESS_METADATA_EXTRACTION": "В процессе", + "INPROGRESS_UPLOADS": "Выполняется загрузка данных", + "TOO_LARGE_UPLOADS": "Большие файлы", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Недостаточный объем памяти", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "Эти файлы не были загружены, так как они превышают максимальный размер, установленный для вашего плана хранения", + "TOO_LARGE_INFO": "Эти файлы не были загружены, так как они превышают наш максимальный размер файла", + "THUMBNAIL_GENERATION_FAILED_INFO": "Эти файлы были загружены, но, к сожалению, мы не смогли сгенерировать для них миниатюры.", + "UPLOAD_TO_COLLECTION": "Загрузить в альбом", + "UNCATEGORIZED": "Без рубрики", + "ARCHIVE": "Архив", + "FAVORITES": "Избранное", + "ARCHIVE_COLLECTION": "Архивный альбом", + "ARCHIVE_SECTION_NAME": "Архив", + "ALL_SECTION_NAME": "Все", + "MOVE_TO_COLLECTION": "Перейти к альбому", + "UNARCHIVE": "Разархивировать", + "UNARCHIVE_COLLECTION": "Разархивировать альбом", + "HIDE_COLLECTION": "Скрыть альбом", + "UNHIDE_COLLECTION": "Показать альбом", + "MOVE": "Подвиньте", + "ADD": "Добавь", + "REMOVE": "Удалять", + "YES_REMOVE": "Да, удалить", + "REMOVE_FROM_COLLECTION": "Удалить из альбома", + "TRASH": "Мусор", + "MOVE_TO_TRASH": "Переместить в корзину", + "TRASH_FILES_MESSAGE": "Выбранные файлы будут удалены из всех альбомов и перемещены в корзину.", + "TRASH_FILE_MESSAGE": "Файл будет удален из всех альбомов и помещен в корзину.", + "DELETE_PERMANENTLY": "Удалить навсегда", + "RESTORE": "Восстанавливать", + "RESTORE_TO_COLLECTION": "Восстановить в альбом", + "EMPTY_TRASH": "Пустой мусор", + "EMPTY_TRASH_TITLE": "Пустой мусор?", + "EMPTY_TRASH_MESSAGE": "Эти файлы будут безвозвратно удалены из вашей учетной записи Ente.", + "LEAVE_SHARED_ALBUM": "Да, уходи", + "LEAVE_ALBUM": "Оставить альбом", + "LEAVE_SHARED_ALBUM_TITLE": "Оставить общий альбом?", + "LEAVE_SHARED_ALBUM_MESSAGE": "Вы покинете альбом, и он перестанет быть видимым для вас.", + "NOT_FILE_OWNER": "Вы не можете удалить файлы из общего альбома", + "CONFIRM_SELF_REMOVE_MESSAGE": "Выбранные элементы будут удалены из этого альбома. Элементы, которые есть только в этом альбоме, будут перемещены в раздел без рубрики.", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Некоторые из удаляемых вами элементов были добавлены другими пользователями, и вы потеряете к ним доступ.", + "SORT_BY_CREATION_TIME_ASCENDING": "Старейший", + "SORT_BY_UPDATION_TIME_DESCENDING": "Последнее обновление", + "SORT_BY_NAME": "Имя", + "COMPRESS_THUMBNAILS": "Сжатие миниатюр", + "THUMBNAIL_REPLACED": "Сжатые миниатюры", + "FIX_THUMBNAIL": "Сжимать", + "FIX_THUMBNAIL_LATER": "Сжимать позже", + "REPLACE_THUMBNAIL_NOT_STARTED": "Некоторые миниатюры ваших видео можно сжать для экономии места. хотите, чтобы мы их сжали?", + "REPLACE_THUMBNAIL_COMPLETED": "Успешно сжаты все миниатюры", + "REPLACE_THUMBNAIL_NOOP": "У вас нет миниатюр, которые можно было бы дополнительно сжать", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Не удалось сжать некоторые из ваших миниатюр, пожалуйста, повторите попытку", + "FIX_CREATION_TIME": "Назначьте время", + "FIX_CREATION_TIME_IN_PROGRESS": "Фиксирующее время", + "CREATION_TIME_UPDATED": "Время обновления файла", + "UPDATE_CREATION_TIME_NOT_STARTED": "Выберите опцию, которую вы хотите использовать", + "UPDATE_CREATION_TIME_COMPLETED": "Успешно обновлены все файлы", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Для некоторых файлов не удалось выполнить обновление времени, пожалуйста, повторите попытку", + "CAPTION_CHARACTER_LIMIT": "не более 5000 символов", + "DATE_TIME_ORIGINAL": "EXIF:дата и времениоригинал", + "DATE_TIME_DIGITIZED": "EXIF:дата и время определены", + "METADATA_DATE": "EXIF:метаданные", + "CUSTOM_TIME": "Пользовательское время", + "REOPEN_PLAN_SELECTOR_MODAL": "Повторно открывайте планы", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Не удалось раскрыть планы", + "INSTALL": "Устанавливать", + "SHARING_DETAILS": "Обмен подробностями", + "MODIFY_SHARING": "Изменить общий доступ", + "ADD_COLLABORATORS": "Добавление соавторов", + "ADD_NEW_EMAIL": "Добавьте новое электронное письмо", + "shared_with_people_zero": "Делитесь с конкретными людьми", + "shared_with_people_one": "Совместно с 1 человеком", + "shared_with_people_other": "Поделился с {{count, number}} люди", + "participants_zero": "Нет участников", + "participants_one": "1 участник", + "participants_other": "{{count, number}} участники", + "ADD_VIEWERS": "Добавить зрителей", + "PARTICIPANTS": "Участники", + "CHANGE_PERMISSIONS_TO_VIEWER": "

{{selectedEmail}} они не смогут добавить больше фотографий в альбом

Они по-прежнему смогут удалять добавленные ими фотографии

", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} сможете добавлять фотографии в альбом", + "CONVERT_TO_VIEWER": "Да, преобразовать в программу просмотра", + "CONVERT_TO_COLLABORATOR": "Да, преобразовать в соавтора", + "CHANGE_PERMISSION": "Изменить разрешение?", + "REMOVE_PARTICIPANT": "Удалить?", + "CONFIRM_REMOVE": "Да, удалить", + "MANAGE": "Управлять", + "ADDED_AS": "Добавлено как", + "COLLABORATOR_RIGHTS": "Участники совместной работы могут добавлять фотографии и видео в общий альбом", + "REMOVE_PARTICIPANT_HEAD": "Удалить участника", + "OWNER": "Владелец", + "COLLABORATORS": "Коллаборационисты", + "ADD_MORE": "Добавить еще", + "VIEWERS": "Зрителей", + "OR_ADD_EXISTING": "Или выберите уже существующий вариант", + "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} будут удалены из альбома

Все добавленные ими фотографии также будут удалены из альбома

", + "NOT_FOUND": "404 - не найден", + "LINK_EXPIRED": "Срок действия ссылки истек", + "LINK_EXPIRED_MESSAGE": "Срок действия этой ссылки либо истек, либо она была отключена!", + "MANAGE_LINK": "Управление ссылкой", + "LINK_TOO_MANY_REQUESTS": "Извините, этот альбом был просмотрен на слишком большом количестве устройств!", + "FILE_DOWNLOAD": "Разрешить загрузку", + "LINK_PASSWORD_LOCK": "Блокировка паролем", + "PUBLIC_COLLECT": "Разрешить добавление фотографий", + "LINK_DEVICE_LIMIT": "Предел устройства", + "NO_DEVICE_LIMIT": "Никто", + "LINK_EXPIRY": "Истечение срока действия ссылки", + "NEVER": "Никогда", + "DISABLE_FILE_DOWNLOAD": "Отключить загрузку", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Вы уверены, что хотите отключить кнопку загрузки файлов?

Зрители по-прежнему могут делать скриншоты или сохранять копии ваших фотографий с помощью внешних инструментов.

", + "MALICIOUS_CONTENT": "Содержит вредоносный контент", + "COPYRIGHT": "Нарушает авторские права человека, которого я уполномочен представлять", + "SHARED_USING": "Совместное использование ", + "ENTE_IO": "ente.io", + "SHARING_REFERRAL_CODE": "Используйте код {{referralCode}} чтобы получить 10 ГБ бесплатно", + "LIVE": "Жить", + "DISABLE_PASSWORD": "Отключить блокировку паролем", + "DISABLE_PASSWORD_MESSAGE": "Вы уверены, что хотите отключить блокировку паролем?", + "PASSWORD_LOCK": "Блокировка паролем", + "LOCK": "Замок", + "DOWNLOAD_UPLOAD_LOGS": "Журналы отладки", + "UPLOAD_FILES": "Файл", + "UPLOAD_DIRS": "Папка", + "UPLOAD_GOOGLE_TAKEOUT": "Еда на вынос из Google", + "DEDUPLICATE_FILES": "Дедуплицировать файлы", + "AUTHENTICATOR_SECTION": "Аутентификатор", + "NO_DUPLICATES_FOUND": "У вас нет дубликатов файлов, которые можно было бы удалить", + "CLUB_BY_CAPTURE_TIME": "Клуб по времени захвата", + "FILES": "файлы", + "EACH": "каждый", + "DEDUPLICATE_BASED_ON_SIZE": "Следующие файлы были заблокированы в зависимости от их размера, пожалуйста, просмотрите и удалите элементы, которые, по вашему мнению, являются дубликатами", + "STOP_ALL_UPLOADS_MESSAGE": "Вы уверены, что хотите остановить все текущие загрузки?", + "STOP_UPLOADS_HEADER": "Остановить загрузку?", + "YES_STOP_UPLOADS": "Да, остановите загрузку", + "STOP_DOWNLOADS_HEADER": "Остановить загрузку?", + "YES_STOP_DOWNLOADS": "Да, остановите загрузку", + "STOP_ALL_DOWNLOADS_MESSAGE": "Вы уверены, что хотите остановить все текущие загрузки?", + "albums_one": "1 Альбом", + "albums_other": "{{count, number}} Альбомы", + "ALL_ALBUMS": "Все альбомы", + "ALBUMS": "Альбомы", + "ALL_HIDDEN_ALBUMS": "Все скрытые альбомы", + "HIDDEN_ALBUMS": "Скрытые альбомы", + "HIDDEN_ITEMS": "Скрытые предметы", + "HIDDEN_ITEMS_SECTION_NAME": "Скрытые_элементы", + "ENTER_TWO_FACTOR_OTP": "Введите 6-значный код из вашего приложения для проверки подлинности.", + "CREATE_ACCOUNT": "Создать аккаунт", + "COPIED": "Скопированный", + "CANVAS_BLOCKED_TITLE": "Не удается создать миниатюру", + "CANVAS_BLOCKED_MESSAGE": "

Похоже, что в вашем браузере отключен доступ к canvas, который необходим для создания миниатюр для ваших фотографий

Пожалуйста, включите доступ к canvas в вашем браузере или воспользуйтесь нашим настольным приложением

", + "WATCH_FOLDERS": "Папки для просмотра", + "UPGRADE_NOW": "Обновите сейчас", + "RENEW_NOW": "Обновите сейчас", + "STORAGE": "Место хранения", + "USED": "использованный", + "YOU": "Вы", + "FAMILY": "Семья", + "FREE": "свободный", + "OF": "от", + "WATCHED_FOLDERS": "Просматриваемые папки", + "NO_FOLDERS_ADDED": "Папки еще не добавлены!", + "FOLDERS_AUTOMATICALLY_MONITORED": "Папки, которые вы добавите сюда, будут автоматически обновлены", + "UPLOAD_NEW_FILES_TO_ENTE": "Загружайте новые файлы в Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Удалить удаленные файлы из Ente", + "ADD_FOLDER": "Добавить папку", + "STOP_WATCHING": "Перестань наблюдать", + "STOP_WATCHING_FOLDER": "Перестать смотреть \"папку\"?", + "STOP_WATCHING_DIALOG_MESSAGE": "Ваши существующие файлы не будут удалены, но Ente прекратит автоматическое обновление связанного альбома Ente при внесении изменений в эту папку.", + "YES_STOP": "Да, остановись", + "MONTH_SHORT": "мо", + "YEAR": "год", + "FAMILY_PLAN": "Семейный план", + "DOWNLOAD_LOGS": "Загрузка журналов", + "DOWNLOAD_LOGS_MESSAGE": "

При этом будут загружены журналы отладки, которые вы можете отправить нам по электронной почте, чтобы помочь в устранении вашей проблемы.

Пожалуйста, обратите внимание, что будут указаны имена файлов, которые помогут отслеживать проблемы с конкретными файлами.

", + "CHANGE_FOLDER": "Изменить папку", + "TWO_MONTHS_FREE": "Получите 2 месяца бесплатно по годовым планам", + "GB": "Гб", + "POPULAR": "Популярный", + "FREE_PLAN_OPTION_LABEL": "Продолжайте пользоваться бесплатной пробной версией", + "FREE_PLAN_DESCRIPTION": "1 ГБ на 1 год", + "CURRENT_USAGE": "Текущее использование составляет {{usage}}", + "WEAK_DEVICE": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.", + "DRAG_AND_DROP_HINT": "Или перетащите в основное окно", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Ваши загруженные данные будут запланированы к удалению, а ваша учетная запись будет удалена безвозвратно.

Это действие необратимо.", + "AUTHENTICATE": "Проверка подлинности", + "UPLOADED_TO_SINGLE_COLLECTION": "Загружено в единую коллекцию", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "Загружено в отдельные коллекции", + "NEVERMIND": "Не берите в голову", + "UPDATE_AVAILABLE": "Доступно обновление", + "UPDATE_INSTALLABLE_MESSAGE": "Новая версия Ente готова к установке.", + "INSTALL_NOW": "Установите сейчас", + "INSTALL_ON_NEXT_LAUNCH": "Установите при следующем запуске", + "UPDATE_AVAILABLE_MESSAGE": "Была выпущена новая версия Ente, но она не может быть автоматически загружена и установлена.", + "DOWNLOAD_AND_INSTALL": "Скачайте и установите", + "IGNORE_THIS_VERSION": "Игнорируйте эту версию", + "TODAY": "Сегодня", + "YESTERDAY": "Вчера", + "NAME_PLACEHOLDER": "Имя...", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Не удается создать альбомы из сочетания файлов и папок", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

Вы перетащили несколько файлов и папок.

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

", + "CHOSE_THEME": "Выберите тему", + "ML_SEARCH": "Распознавание лиц", + "ENABLE_ML_SEARCH_DESCRIPTION": "

Это включит машинное обучение на устройстве и поиск по лицу, которые начнут анализировать ваши загруженные фотографии локально.

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

Если вы включаете это в первый раз, мы также попросим вашего разрешения на обработку данных о лице.

", + "ML_MORE_DETAILS": "Более подробная информация", + "ENABLE_FACE_SEARCH": "Включить распознавание лиц", + "ENABLE_FACE_SEARCH_TITLE": "Включить распознавание лиц?", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

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

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

", + "DISABLE_BETA": "Приостановить распознавание", + "DISABLE_FACE_SEARCH": "Отключить распознавание лиц", + "DISABLE_FACE_SEARCH_TITLE": "Отключить распознавание лиц?", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Программа Ente прекратит обработку геометрии лица.

При желании вы можете снова включить функцию распознавания лиц, так что эта операция безопасна.

", + "ADVANCED": "Передовой", + "FACE_SEARCH_CONFIRMATION": "Я понимаю и хочу позволить Ente обрабатывать геометрию грани", + "LABS": "Лаборатории", + "YOURS": "твой", + "PASSPHRASE_STRENGTH_WEAK": "Надежность пароля: слабая", + "PASSPHRASE_STRENGTH_MODERATE": "Надежность пароля: Умеренная", + "PASSPHRASE_STRENGTH_STRONG": "Надежность пароля: Надежный", + "PREFERENCES": "Предпочтения", + "LANGUAGE": "Язык", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Недопустимый каталог экспорта", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

Выбранный вами каталог экспорта не существует.

Пожалуйста, выберите правильный каталог.

", + "SUBSCRIPTION_VERIFICATION_ERROR": "Не удалось подтвердить подписку", "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "B": "B", + "KB": "БЗ", + "MB": "Мегабайт", + "GB": "Гб", + "TB": "Терабайт" }, "AFTER_TIME": { - "HOUR": "", - "DAY": "", - "WEEK": "", - "MONTH": "", - "YEAR": "" + "HOUR": "через час", + "DAY": "через день", + "WEEK": "через неделю", + "MONTH": "через месяц", + "YEAR": "через год" }, - "COPY_LINK": "", - "DONE": "", - "LINK_SHARE_TITLE": "", - "REMOVE_LINK": "", - "CREATE_PUBLIC_SHARING": "", - "PUBLIC_LINK_CREATED": "", - "PUBLIC_LINK_ENABLED": "", - "COLLECT_PHOTOS": "", - "PUBLIC_COLLECT_SUBTEXT": "", - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", + "COPY_LINK": "Скопировать ссылку", + "DONE": "Сделано", + "LINK_SHARE_TITLE": "Или поделитесь ссылкой", + "REMOVE_LINK": "Удалить ссылку", + "CREATE_PUBLIC_SHARING": "Создать общедоступную ссылку", + "PUBLIC_LINK_CREATED": "Создана общедоступная ссылка", + "PUBLIC_LINK_ENABLED": "Включена общедоступная ссылка", + "COLLECT_PHOTOS": "Собирайте фотографии", + "PUBLIC_COLLECT_SUBTEXT": "Разрешите пользователям, у которых есть ссылка, также добавлять фотографии в общий альбом.", + "STOP_EXPORT": "Остановка", + "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} синхронизированные элементы", + "MIGRATING_EXPORT": "Подготовка...", + "RENAMING_COLLECTION_FOLDERS": "Переименование папок альбомов...", + "TRASHING_DELETED_FILES": "Удаление удаленных файлов...", + "TRASHING_DELETED_COLLECTIONS": "Удаление удаленных альбомов...", "EXPORT_NOTIFICATION": { - "START": "", - "IN_PROGRESS": "", - "FINISH": "", - "UP_TO_DATE": "" + "START": "Экспорт начат", + "IN_PROGRESS": "Экспорт уже осуществляется", + "FINISH": "Готовый экспорт", + "UP_TO_DATE": "Нет новых файлов для экспорта" }, - "CONTINUOUS_EXPORT": "", - "TOTAL_ITEMS": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", - "DELETE_ACCOUNT_REASON_LABEL": "", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "CONTINUOUS_EXPORT": "Непрерывная синхронизация", + "TOTAL_ITEMS": "Всего товаров", + "PENDING_ITEMS": "Отложенные пункты", + "EXPORT_STARTING": "Запуск экспорта...", + "DELETE_ACCOUNT_REASON_LABEL": "Какова основная причина, по которой вы удаляете свою учетную запись?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Выберите причину", "DELETE_REASON": { - "MISSING_FEATURE": "", - "BROKEN_BEHAVIOR": "", - "FOUND_ANOTHER_SERVICE": "", - "NOT_LISTED": "" + "MISSING_FEATURE": "В нем отсутствует ключевая функция, которая мне нужна", + "BROKEN_BEHAVIOR": "Приложение или определенная функция ведут себя не так, как я думаю, должно", + "FOUND_ANOTHER_SERVICE": "Я нашел другой сервис, который мне нравится больше", + "NOT_LISTED": "Моя причина не указана в списке" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "", - "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", - "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", - "CONFIRM_DELETE_ACCOUNT": "", - "FEEDBACK_REQUIRED": "", - "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", - "RECOVER_TWO_FACTOR": "", - "at": "", - "AUTH_NEXT": "", - "AUTH_DOWNLOAD_MOBILE_APP": "", - "HIDDEN": "", + "DELETE_ACCOUNT_FEEDBACK_LABEL": "Нам жаль, что вы уходите. Пожалуйста, объясните, почему вы уходите, чтобы помочь нам совершенствоваться.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Обратная связь", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Да, я хочу навсегда удалить эту учетную запись и все ее данные", + "CONFIRM_DELETE_ACCOUNT": "Подтвердите удаление учетной записи", + "FEEDBACK_REQUIRED": "Пожалуйста, помогите нам с этой информацией", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Что другой сервис делает лучше?", + "RECOVER_TWO_FACTOR": "Восстановление двухфакторного", + "at": "около", + "AUTH_NEXT": "следующий", + "AUTH_DOWNLOAD_MOBILE_APP": "Скачайте наше мобильное приложение, чтобы управлять своими секретами", + "HIDDEN": "Скрытый", "HIDE": "Скрыть", "UNHIDE": "Показать", - "UNHIDE_TO_COLLECTION": "", + "UNHIDE_TO_COLLECTION": "Отобразить в альбоме", "SORT_BY": "Сортировать по", "NEWEST_FIRST": "Сначала новые", "OLDEST_FIRST": "Сначала старые", - "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "Не удалось просмотреть этот файл. Нажмите здесь, чтобы загрузить оригинал.", "SELECT_COLLECTION": "Выбрать альбом", "PIN_ALBUM": "Закрепить альбом", "UNPIN_ALBUM": "Открепить альбом", @@ -615,27 +615,40 @@ "TRANSFORM": "Преобразовать", "COLORS": "Цвета", "FLIP": "Перевернуть", - "ROTATION": "", + "ROTATION": "Вращение", "RESET": "Сбросить", "PHOTO_EDITOR": "Редактор фото", - "FASTER_UPLOAD": "", - "FASTER_UPLOAD_DESCRIPTION": "", + "FASTER_UPLOAD": "Более быстрая загрузка данных", + "FASTER_UPLOAD_DESCRIPTION": "Загрузка маршрута через близлежащие серверы", "MAGIC_SEARCH_STATUS": "Статус волшебного поиска", "INDEXED_ITEMS": "Индексированные элементы", "CAST_ALBUM_TO_TV": "Воспроизвести альбом на ТВ", - "ENTER_CAST_PIN_CODE": "", - "PAIR_DEVICE_TO_TV": "", - "TV_NOT_FOUND": "", - "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", - "PAIR_WITH_PIN": "", - "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", - "VISIT_CAST_ENTE_IO": "", - "CAST_AUTO_PAIR_FAILED": "", - "CACHE_DIRECTORY": "", - "PASSKEYS": "", - "FREEHAND": "", - "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "ENTER_CAST_PIN_CODE": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.", + "PAIR_DEVICE_TO_TV": "Сопряжение устройств", + "TV_NOT_FOUND": "Телевизор не найден. Вы правильно ввели PIN-код?", + "AUTO_CAST_PAIR": "Автоматическое сопряжение", + "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Автоматическое сопряжение требует подключения к серверам Google и работает только с устройствами, поддерживающими Chromecast. Google не будет получать конфиденциальные данные, такие как ваши фотографии.", + "PAIR_WITH_PIN": "Соединение с помощью булавки", + "CHOOSE_DEVICE_FROM_BROWSER": "Выберите устройство, совместимое с cast, во всплывающем окне браузера.", + "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Сопряжение с помощью PIN-кода работает на любом устройстве с большим экраном, на котором вы хотите воспроизвести свой альбом.", + "VISIT_CAST_ENTE_IO": "Перейдите на страницу {{url}} на устройстве, которое вы хотите подключить.", + "CAST_AUTO_PAIR_FAILED": "Не удалось выполнить автоматическое сопряжение Chromecast. Пожалуйста, попробуйте снова.", + "CACHE_DIRECTORY": "Папка кэша", + "FREEHAND": "От руки", + "APPLY_CROP": "Применить обрезку", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Перед сохранением необходимо выполнить по крайней мере одно преобразование или корректировку цвета.", + "PASSKEYS": "Пароли доступа", + "DELETE_PASSKEY": "Удалить пароль", + "DELETE_PASSKEY_CONFIRMATION": "Вы уверены, что хотите удалить этот пароль? Это действие необратимо.", + "RENAME_PASSKEY": "Переименовать пароль", + "ADD_PASSKEY": "Добавить пароль", + "ENTER_PASSKEY_NAME": "Введите имя ключа доступа", + "PASSKEYS_DESCRIPTION": "Пароли доступа - это современный и безопасный дополнительный фактор для вашей учетной записи Ente. Для удобства и безопасности они используют биометрическую аутентификацию на устройстве.", + "CREATED_AT": "Созданный в", + "PASSKEY_LOGIN_FAILED": "Не удалось войти с помощью пароля", + "PASSKEY_LOGIN_URL_INVALID": "Неверный URL-адрес для входа в систему.", + "PASSKEY_LOGIN_ERRORED": "При входе в систему с помощью пароля произошла ошибка.", + "TRY_AGAIN": "Пробовать снова", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Следуйте инструкциям в вашем браузере, чтобы продолжить вход в систему.", + "LOGIN_WITH_PASSKEY": "Войдите в систему с помощью пароля" } diff --git a/web/apps/photos/public/locales/sv-SE/translation.json b/web/apps/photos/public/locales/sv-SE/translation.json index 550780b44..6963261ef 100644 --- a/web/apps/photos/public/locales/sv-SE/translation.json +++ b/web/apps/photos/public/locales/sv-SE/translation.json @@ -421,7 +421,7 @@ "AUTHENTICATOR_SECTION": "", "NO_DUPLICATES_FOUND": "", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "Filer", + "FILES": "filer", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/th-TH/translation.json b/web/apps/photos/public/locales/th-TH/translation.json index fbc0b937a..888ed7093 100644 --- a/web/apps/photos/public/locales/th-TH/translation.json +++ b/web/apps/photos/public/locales/th-TH/translation.json @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/tr-TR/translation.json b/web/apps/photos/public/locales/tr-TR/translation.json index fbc0b937a..888ed7093 100644 --- a/web/apps/photos/public/locales/tr-TR/translation.json +++ b/web/apps/photos/public/locales/tr-TR/translation.json @@ -634,8 +634,21 @@ "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "CACHE_DIRECTORY": "", - "PASSKEYS": "", "FREEHAND": "", "APPLY_CROP": "", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PASSKEYS": "", + "DELETE_PASSKEY": "", + "DELETE_PASSKEY_CONFIRMATION": "", + "RENAME_PASSKEY": "", + "ADD_PASSKEY": "", + "ENTER_PASSKEY_NAME": "", + "PASSKEYS_DESCRIPTION": "", + "CREATED_AT": "", + "PASSKEY_LOGIN_FAILED": "", + "PASSKEY_LOGIN_URL_INVALID": "", + "PASSKEY_LOGIN_ERRORED": "", + "TRY_AGAIN": "", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", + "LOGIN_WITH_PASSKEY": "" } diff --git a/web/apps/photos/public/locales/zh-CN/translation.json b/web/apps/photos/public/locales/zh-CN/translation.json index 6e513f24b..3e764e806 100644 --- a/web/apps/photos/public/locales/zh-CN/translation.json +++ b/web/apps/photos/public/locales/zh-CN/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "安卓, iOS, 网页端, 桌面端", "LOGIN": "登录", "SIGN_UP": "注册", - "NEW_USER": "刚来到 ente", + "NEW_USER": "刚来到 Ente", "EXISTING_USER": "现有用户", "ENTER_NAME": "输入名字", "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", @@ -65,13 +65,13 @@ "5": "备份完成" }, "FILE_NOT_UPLOADED_LIST": "以下文件未上传", - "SUBSCRIPTION_EXPIRED": "您的订阅已过期", + "SUBSCRIPTION_EXPIRED": "订阅已过期", "SUBSCRIPTION_EXPIRED_MESSAGE": "您的订阅已过期,请 续期", "STORAGE_QUOTA_EXCEEDED": "已超出存储限制", "INITIAL_LOAD_DELAY_WARNING": "第一次加载可能需要一些时间", - "USER_DOES_NOT_EXIST": "抱歉,找不到该电子邮件的用户", + "USER_DOES_NOT_EXIST": "抱歉,找不到使用该电子邮件的用户", "NO_ACCOUNT": "没有账号", - "ACCOUNT_EXISTS": "已有账户", + "ACCOUNT_EXISTS": "已有账号", "CREATE": "创建", "DOWNLOAD": "下载", "DOWNLOAD_OPTION": "下载 (D)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "独立相册", "SESSION_EXPIRED_MESSAGE": "您的会话已过期,请重新登录以继续", "SESSION_EXPIRED": "会话已过期", - "PASSWORD_GENERATION_FAILED": "您的浏览器无法生成一个符合ente加密标准的强密钥,请尝试使用移动应用程序或其他浏览器", + "PASSWORD_GENERATION_FAILED": "您的浏览器无法生成一个符合Ente加密标准的强密钥,请尝试使用移动应用程序或其他浏览器", "CHANGE_PASSWORD": "修改密码", "GO_BACK": "返回", "RECOVERY_KEY": "恢复密钥", @@ -122,7 +122,7 @@ "INCORRECT_RECOVERY_KEY": "不正确的恢复密钥", "SORRY": "抱歉", "NO_RECOVERY_KEY_MESSAGE": "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "请用您注册ente账户的电子邮箱发一封邮件给 {{emailID}}", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "请用您注册Ente账户的电子邮箱发一封邮件给 {{emailID}}", "CONTACT_SUPPORT": "联系支持", "REQUEST_FEATURE": "功能建议", "SUPPORT": "支持", @@ -250,27 +250,27 @@ "NO_EXIF": "无 EXIF 数据", "EXIF": "EXIF", "ISO": "ISO", - "TWO_FACTOR": "双因素", - "TWO_FACTOR_AUTHENTICATION": "双因素认证", + "TWO_FACTOR": "双重认证", + "TWO_FACTOR_AUTHENTICATION": "双重认证", "TWO_FACTOR_QR_INSTRUCTION": "使用您最喜欢的身份验证器应用程序(2FA)扫描下面的二维码", "ENTER_CODE_MANUALLY": "请手动输入代码", "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "请在您最喜欢的验证器应用中输入此代码", "SCAN_QR_CODE": "改为扫描二维码", - "ENABLE_TWO_FACTOR": "启用双因素认证", + "ENABLE_TWO_FACTOR": "启用双重认证", "ENABLE": "启用", - "LOST_DEVICE": "丢失了双因素认证设备", + "LOST_DEVICE": "丢失了双重认证设备", "INCORRECT_CODE": "代码错误", "TWO_FACTOR_INFO": "登录您的账户不仅需要您的电子邮件和密码,还需要额外的安全层", - "DISABLE_TWO_FACTOR_LABEL": "禁用双因素认证", + "DISABLE_TWO_FACTOR_LABEL": "禁用双重认证", "UPDATE_TWO_FACTOR_LABEL": "更新您的身份验证器设备", "DISABLE": "禁用", "RECONFIGURE": "重新配置", - "UPDATE_TWO_FACTOR": "更新双因素认证", + "UPDATE_TWO_FACTOR": "更新双重认证", "UPDATE_TWO_FACTOR_MESSAGE": "向前继续将使之前配置的任何身份验证器无效", "UPDATE": "更新", - "DISABLE_TWO_FACTOR": "禁用双因素认证", - "DISABLE_TWO_FACTOR_MESSAGE": "您确定要禁用您的双因素认证吗?", - "TWO_FACTOR_DISABLE_FAILED": "禁用双因素认证失败,请再试一次", + "DISABLE_TWO_FACTOR": "禁用双重认证", + "DISABLE_TWO_FACTOR_MESSAGE": "您确定要禁用您的双重认证吗?", + "TWO_FACTOR_DISABLE_FAILED": "禁用双重认证失败,请再试一次", "EXPORT_DATA": "导出数据", "SELECT_FOLDER": "选择文件夹", "DESTINATION": "目标位置", @@ -278,10 +278,10 @@ "LAST_EXPORT_TIME": "最后一次导出时间", "EXPORT_AGAIN": "重新同步", "LOCAL_STORAGE_NOT_ACCESSIBLE": "无法访问本地存储", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "您的浏览器或插件阻止 ente 将数据保存到本地存储。 请在切换浏览模式后再尝试加载此页面。", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "您的浏览器或插件阻止 Ente 将数据保存到本地存储。 请在切换浏览模式后再尝试加载此页面。", "SEND_OTT": "发送 OTP", "EMAIl_ALREADY_OWNED": "电子邮箱已被注册", - "ETAGS_BLOCKED": "

由于您的浏览器配置,我们无法上传以下文件。

请禁用任何可能阻止ente 使用 eTags 上传大文件的附加组件, 或者使用我们的 桌面应用程序 获取更可靠的导入体验。

", + "ETAGS_BLOCKED": "

由于您的浏览器配置,我们无法上传以下文件。

请禁用任何可能阻止Ente 使用 eTags 上传大文件的附加组件, 或者使用我们的 桌面应用程序 获取更可靠的导入体验。

", "SKIPPED_VIDEOS_INFO": "

目前,我们不支持在公共链接内添加视频。

若要分享视频,请 注册 并通过电子邮件与预定收件人分享。

", "LIVE_PHOTOS_DETECTED": "Live Photos 中的照片和视频文件已合并为一个文件", "RETRY_FAILED": "重试上传失败的文件", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "不支持的文件", "SUCCESSFUL_UPLOADS": "上传成功", "SKIPPED_INFO": "跳过这些,因为在同一相册中有具有匹配名称的文件", - "UNSUPPORTED_INFO": "ente 尚不支持这些文件格式", + "UNSUPPORTED_INFO": "Ente 尚不支持这些文件格式", "BLOCKED_UPLOADS": "已阻止上传", "SKIPPED_VIDEOS": "已跳过的视频", "INPROGRESS_METADATA_EXTRACTION": "进行中", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "恢复到相册", "EMPTY_TRASH": "清空回收站", "EMPTY_TRASH_TITLE": "要清空回收站吗?", - "EMPTY_TRASH_MESSAGE": "这些文件将从您的 ente 账户中永久删除。", + "EMPTY_TRASH_MESSAGE": "这些文件将从您的 Ente 账户中永久删除。", "LEAVE_SHARED_ALBUM": "是,离开", "LEAVE_ALBUM": "离开相册", "LEAVE_SHARED_ALBUM_TITLE": "要离开共享相册吗?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "缩略图已压缩", "FIX_THUMBNAIL": "压缩", "FIX_THUMBNAIL_LATER": "稍后压缩", - "REPLACE_THUMBNAIL_NOT_STARTED": "您的一些视频缩略图可以被压缩以节省空间,您想要ente 压缩它们吗?", + "REPLACE_THUMBNAIL_NOT_STARTED": "您的一些视频缩略图可以被压缩以节省空间,您想要Ente 压缩它们吗?", "REPLACE_THUMBNAIL_COMPLETED": "已成功压缩所有缩略图", "REPLACE_THUMBNAIL_NOOP": "您没有可以进一步压缩的缩略图", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "无法压缩您的一些缩略图,请重试", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "观看文件夹", "NO_FOLDERS_ADDED": "尚未添加任何文件夹!", "FOLDERS_AUTOMATICALLY_MONITORED": "您在此处添加的文件夹将自动监控", - "UPLOAD_NEW_FILES_TO_ENTE": "上传新文件至 ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "从ente 移除已删除的文件", + "UPLOAD_NEW_FILES_TO_ENTE": "上传新文件至 Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "从Ente 移除已删除的文件", "ADD_FOLDER": "添加文件夹", "STOP_WATCHING": "停止监控", "STOP_WATCHING_FOLDER": "要停止监控文件夹?", - "STOP_WATCHING_DIALOG_MESSAGE": "您现有的文件不会被删除,但 ente 将停止自动更新链接的 ente 相册在此文件夹中的更改。", + "STOP_WATCHING_DIALOG_MESSAGE": "您现有的文件不会被删除,但 Ente 将停止自动更新链接的 Ente 相册在此文件夹中的更改。", "YES_STOP": "是的,停止", "MONTH_SHORT": "月", "YEAR": "年", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "继续免费试用", "FREE_PLAN_DESCRIPTION": "1 GB 1年", "CURRENT_USAGE": "当前使用量是 {{usage}}", - "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录ente,或下载ente移动/桌面应用程序。", - "DRAG_AND_DROP_HINT": "或者拖动并拖动到 ente 窗口", + "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。", + "DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "您上传的数据将被安排删除,您的账户将被永久删除。

此操作不可逆。", "AUTHENTICATE": "身份认证", "UPLOADED_TO_SINGLE_COLLECTION": "已上传到单个收藏", "UPLOADED_TO_SEPARATE_COLLECTIONS": "已上传到单独收藏", "NEVERMIND": "没关系", "UPDATE_AVAILABLE": "有可用的更新", - "UPDATE_INSTALLABLE_MESSAGE": "新版本的 ente 已准备好安装。", + "UPDATE_INSTALLABLE_MESSAGE": "新版本的 Ente 已准备好安装。", "INSTALL_NOW": "立即安装", "INSTALL_ON_NEXT_LAUNCH": "在下次启动时安装", - "UPDATE_AVAILABLE_MESSAGE": "新版本的 ente 已发布,但无法自动下载和安装。", + "UPDATE_AVAILABLE_MESSAGE": "新版本的 Ente 已发布,但无法自动下载和安装。", "DOWNLOAD_AND_INSTALL": "下载并安装", "IGNORE_THIS_VERSION": "忽略该版本", "TODAY": "今天", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "更多详情", "ENABLE_FACE_SEARCH": "启用面部搜索", "ENABLE_FACE_SEARCH_TITLE": "要启用面部搜索吗?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

如果您启用面部搜索,ente 将从照片中提取脸部几何形状。 这将发生在您的设备上,任何生成的生物测定数据都将是端到端加密的。

请单击此处以在我们的隐私政策中了解有关此功能的更多详细信息

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

如果您启用面部搜索,Ente 将从照片中提取脸部几何形状。 这将发生在您的设备上,任何生成的生物测定数据都将是端到端加密的。

请单击此处以在我们的隐私政策中了解有关此功能的更多详细信息

", "DISABLE_BETA": "禁用beta", "DISABLE_FACE_SEARCH": "禁用面部搜索", "DISABLE_FACE_SEARCH_TITLE": "要禁用面部搜索吗?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente 将停止处理面部的几何形状, 并将禁用 ML 搜索 (测试版)

如果您愿意,您可以重新启用面部搜索,因此该操作是安全的。

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente 将停止处理面部的几何形状, 并将禁用 ML 搜索 (测试版)

如果您愿意,您可以重新启用面部搜索,因此该操作是安全的。

", "ADVANCED": "高级设置", - "FACE_SEARCH_CONFIRMATION": "我理解,并希望允许ente处理面部几何形状", + "FACE_SEARCH_CONFIRMATION": "我理解,并希望允许Ente处理面部几何形状", "LABS": "实验室", "YOURS": "你的", "PASSPHRASE_STRENGTH_WEAK": "密码强度:较弱", @@ -569,7 +569,7 @@ "CONFIRM_DELETE_ACCOUNT": "确认删除账户", "FEEDBACK_REQUIRED": "请帮助我们了解这个信息", "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "其他服务做得更好?", - "RECOVER_TWO_FACTOR": "恢复双因素认证", + "RECOVER_TWO_FACTOR": "恢复双重认证", "at": "在", "AUTH_NEXT": "下一个", "AUTH_DOWNLOAD_MOBILE_APP": "下载我们的移动应用程序来管理您的密钥", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "实况照片", "CONVERT": "转换", "CONFIRM_EDITOR_CLOSE_MESSAGE": "您确定要关闭编辑器吗?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "下载已编辑的图片或将副本保存到 ente 以保留您的更改。", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "下载已编辑的图片或将副本保存到 Ente 以保留您的更改。", "BRIGHTNESS": "亮度", "CONTRAST": "对比度", "SATURATION": "饱和度", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "垂直翻转", "FLIP_HORIZONTALLY": "水平翻转", "DOWNLOAD_EDITED": "下载已编辑图片", - "SAVE_A_COPY_TO_ENTE": "保存副本到 ente", + "SAVE_A_COPY_TO_ENTE": "保存副本到 Ente", "RESTORE_ORIGINAL": "复原", "TRANSFORM": "转换", "COLORS": "颜色", @@ -631,11 +631,24 @@ "PAIR_WITH_PIN": "用 PIN 配对", "CHOOSE_DEVICE_FROM_BROWSER": "从浏览器弹出窗口中选择兼容 Cast 的设备。", "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "用 PIN 配对适用于任何大屏幕设备,您可以在这些设备上播放您的相册。", - "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 cast.ente.io 。", + "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 {{url}} 。", "CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。", "CACHE_DIRECTORY": "缓存文件夹", - "PASSKEYS": "通行密钥", "FREEHAND": "手画", "APPLY_CROP": "应用裁剪", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。" + "PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。", + "PASSKEYS": "通行密钥", + "DELETE_PASSKEY": "删除通行密钥", + "DELETE_PASSKEY_CONFIRMATION": "您确定要删除此通行密钥吗?此操作是不可逆的。", + "RENAME_PASSKEY": "重命名通行密钥", + "ADD_PASSKEY": "添加通行密钥", + "ENTER_PASSKEY_NAME": "输入该通行密钥的名称", + "PASSKEYS_DESCRIPTION": "通行密钥是您 Ente 账户的现代、安全的第二因素。通行密钥使用设备上的生物识别认证,这既方便又安全。", + "CREATED_AT": "创建于", + "PASSKEY_LOGIN_FAILED": "通行密钥登录失败", + "PASSKEY_LOGIN_URL_INVALID": "该登录 URL 无效", + "PASSKEY_LOGIN_ERRORED": "使用通行密钥登录时出错。", + "TRY_AGAIN": "重试", + "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "按照浏览器中提示的步骤继续登录。", + "LOGIN_WITH_PASSKEY": "使用通行密钥来登录" } diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx index f477ae9bf..894d2a194 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx @@ -10,9 +10,10 @@ import { loadSender } from "@ente/shared/hooks/useCastSender"; import { addLogLine } from "@ente/shared/logging"; import castGateway from "@ente/shared/network/cast"; import { logError } from "@ente/shared/sentry"; -import { Typography } from "@mui/material"; +import { Link, Typography } from "@mui/material"; import { t } from "i18next"; import { useEffect, useState } from "react"; +import { Trans } from "react-i18next"; import { Collection } from "types/collection"; import { v4 as uuidv4 } from "uuid"; @@ -220,12 +221,26 @@ export default function AlbumCastDialog(props: Props) { )} {view === "pin" && ( <> - {t("VISIT_CAST_ENTE_IO")} + + + ), + }} + values={{ url: "cast.ente.io" }} + /> + {t("ENTER_CAST_PIN_CODE")} diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx index 3324be5c4..ce2da895c 100644 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ b/web/apps/photos/src/components/ExportInProgress.tsx @@ -7,11 +7,11 @@ import { Button, DialogActions, DialogContent, + LinearProgress, styled, } from "@mui/material"; import { ExportStage } from "constants/export"; import { t } from "i18next"; -import { ProgressBar } from "react-bootstrap"; import { Trans } from "react-i18next"; import { ExportProgress } from "types/export"; @@ -69,21 +69,19 @@ export default function ExportInProgress(props: Props) { )} - + {showIndeterminateProgress() ? ( + + ) : ( + + )} diff --git a/web/apps/photos/src/components/ExportModal.tsx b/web/apps/photos/src/components/ExportModal.tsx index 142c00743..211a70b1b 100644 --- a/web/apps/photos/src/components/ExportModal.tsx +++ b/web/apps/photos/src/components/ExportModal.tsx @@ -98,8 +98,8 @@ export default function ExportModal(props: Props) { // HELPER FUNCTIONS // ======================= - const verifyExportFolderExists = () => { - if (!exportService.exportFolderExists(exportFolder)) { + const verifyExportFolderExists = async () => { + if (!(await exportService.exportFolderExists(exportFolder))) { appContext.setDialogMessage( getExportDirectoryDoesNotExistMessage(), ); @@ -109,7 +109,7 @@ export default function ExportModal(props: Props) { const syncExportRecord = async (exportFolder: string): Promise => { try { - if (!exportService.exportFolderExists(exportFolder)) { + if (!(await exportService.exportFolderExists(exportFolder))) { const pendingExports = await exportService.getPendingExports(null); setPendingExports(pendingExports); @@ -145,9 +145,9 @@ export default function ExportModal(props: Props) { } }; - const toggleContinuousExport = () => { + const toggleContinuousExport = async () => { try { - verifyExportFolderExists(); + await verifyExportFolderExists(); const newContinuousExport = !continuousExport; if (newContinuousExport) { exportService.enableContinuousExport(); @@ -162,7 +162,7 @@ export default function ExportModal(props: Props) { const startExport = async () => { try { - verifyExportFolderExists(); + await verifyExportFolderExists(); await exportService.scheduleExport(); } catch (e) { if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx new file mode 100644 index 000000000..62c31539a --- /dev/null +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -0,0 +1,264 @@ +import DialogBox from "@ente/shared/components/DialogBox/"; +import { + Button, + FormControl, + FormControlLabel, + FormLabel, + LinearProgress, + Radio, + RadioGroup, +} from "@mui/material"; +import { ComfySpan } from "components/ExportInProgress"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import { GalleryContext } from "pages/gallery"; +import React, { useContext, useEffect, useState } from "react"; +import { updateCreationTimeWithExif } from "services/updateCreationTimeWithExif"; +import { EnteFile } from "types/file"; +import EnteDateTimePicker from "./EnteDateTimePicker"; + +export interface FixCreationTimeAttributes { + files: EnteFile[]; +} + +type Step = "running" | "completed" | "completed-with-errors"; + +export type FixOption = + | "date-time-original" + | "date-time-digitized" + | "metadata-date" + | "custom-time"; + +interface FormValues { + option: FixOption; + /** + * Date.toISOString() + * + * Formik doesn't have native support for JS dates, so we instead keep the + * corresponding date's ISO string representation as the form state. + */ + customTimeString: string; +} + +interface FixCreationTimeProps { + isOpen: boolean; + show: () => void; + hide: () => void; + attributes: FixCreationTimeAttributes; +} + +const FixCreationTime: React.FC = (props) => { + const [step, setStep] = useState(); + const [progressTracker, setProgressTracker] = useState({ + current: 0, + total: 0, + }); + + const galleryContext = useContext(GalleryContext); + + useEffect(() => { + // TODO (MR): Not sure why this is needed + if (props.attributes && props.isOpen && step !== "running") { + setStep(undefined); + } + }, [props.isOpen]); + + const onSubmit = async (values: FormValues) => { + console.log({ values }); + setStep("running"); + const completedWithErrors = await updateCreationTimeWithExif( + props.attributes.files, + values.option, + new Date(values.customTimeString), + setProgressTracker, + ); + setStep(completedWithErrors ? "completed-with-errors" : "completed"); + await galleryContext.syncWithRemote(); + }; + + const title = + step === "running" + ? t("FIX_CREATION_TIME_IN_PROGRESS") + : t("FIX_CREATION_TIME"); + + const message = messageForStep(step); + + if (!props.attributes) { + return <>; + } + + return ( + +
+ {message &&
{message}
} + + {step === "running" && ( + + )} + + +
+
+ ); +}; + +export default FixCreationTime; + +const messageForStep = (step?: Step) => { + switch (step) { + case undefined: + return undefined; + case "running": + return undefined; + case "completed": + return t("UPDATE_CREATION_TIME_COMPLETED"); + case "completed-with-errors": + return t("UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR"); + } +}; + +interface OptionsFormProps { + step?: Step; + onSubmit: (values: FormValues) => void | Promise; + hide: () => void; +} + +const OptionsForm: React.FC = ({ step, onSubmit, hide }) => { + const { values, handleChange, handleSubmit } = useFormik({ + initialValues: { + option: "date-time-original", + customTimeString: new Date().toISOString(), + }, + validateOnBlur: false, + onSubmit, + }); + + return ( + <> + {(step === undefined || step === "completed-with-errors") && ( +
+
+ + + {t("UPDATE_CREATION_TIME_NOT_STARTED")} + + + + } + label={t("DATE_TIME_ORIGINAL")} + /> + } + label={t("DATE_TIME_DIGITIZED")} + /> + } + label={t("METADATA_DATE")} + /> + } + label={t("CUSTOM_TIME")} + /> + + {values.option === "custom-time" && ( + + handleChange("customTimeString")( + d.toISOString(), + ) + } + /> + )} + +
+ )} +